Files
RustPython/crates/codegen/src/compile.rs
T
Jeong, YunWon 300d8bdc02 Honor inline values, custom mro, and type lookup (#8729)
* Add inline values flags and special-method lookup

lookup_maybe_method clears AttributeError from descr_get.
slot_tp_richcompare treats a miss as NotImplemented.
hackcheck rejects object.__setattr__ on types.
Heap types with a managed dict and itemsize 0 get
INLINE_VALUES; _testinternalcapi.has_inline_values
reports the valid bit.
super is a base type; subclass __get__ binds the instance.
__classcell__/__qualname__ may share a name with a slot.
Unset member access uses the type's fully qualified name.

Assisted-by: Grok:grok-4.6

* Call custom mro and descr_get for type.__new__

type.mro() computes C3 from the current bases.
A metaclass other than type supplies the MRO via mro().
super() compares obj.__class__ to the instance type.
slot_tp_new looks up __new__ through descr_get.
is_subtype walks the stored MRO without an identity shortcut.
__slots__ names are sorted so __class__ assignment matches.

Assisted-by: Grok:grok-4.6

* Expose _testinternalcapi recursion and rare events

get_recursion_depth feeds test.support.infinite_recursion.
SPECIALIZATION_* and SHARED_KEYS_MAX_SIZE match the
adaptive counters. Rare-event hooks increment on
__class__/__bases__/function/builtins writes.

Assisted-by: Grok:grok-4.6

* Add remaining _testinternalcapi code and locale APIs

Expose get_co_localskinds, get_code_var_counts, verify_stateless_code,
code_returns_only_none, normalize_path, EncodeLocaleEx, DecodeLocaleEx,
and run_in_subinterp_with_config. Mark unimplemented compiler IR and
specialization monitoring tests as expectedFailure.

Assisted-by: Grok:grok-4.6

* Fix lint for _testinternalcapi identifiers

Add CPython identifier spellings to the cspell dictionary and apply
the rustfmt signature that CI rewrote.

Assisted-by: Grok:grok-4.6

* Expand _testinternalcapi with remaining test APIs

Add constants, compiler_cleandoc, dict_getitem_knownhash, time
helpers, interpreter/frame accessors, configs, static types, a
persistent hamt map, and GC/size counters used by the test suite.

Assisted-by: Grok:grok-4.6

* Expose compiler IR APIs in _testinternalcapi

Add InstructionSequence and compiler_codegen, optimize_cfg, and
assemble_code_object. Drop expectedFailure marks on tests that pass.

Assisted-by: Grok:grok-4.6

* Match optimize_cfg consts and addop locations

Write optimized consts back into the caller's list, mark SETUP_*
pseudo-ops as having an argument, and store addop locations in
instruction-sequence field order. Drop expectedFailure marks on
tests that pass.

Assisted-by: Grok:grok-4.6

* Fix hamt API, SETUP_* HAS_ARG, and FOR_ITER events

Honor explicit pseudo flags when generating opcode metadata so
SETUP_FINALLY/CLEANUP/WITH keep HAS_ARG after regeneration.

Complete the _testinternalcapi hamt mapping: identity on
set/delete, richcompare, weakrefs, subscript, contains, and
length-bearing iterators.

Fire RAISE/STOP_ITERATION for FOR_ITER StopIteration, and skip
both END_FOR and POP_ITER for the exhausted-branch destination.

Assisted-by: Grok:grok-4.6

* Skip FOR_ITER RAISE for sequence iterators

List/range/tuple exhaustion matches FOR_ITER_LIST/RANGE/TUPLE and
must not emit a monitoring RAISE. That event stopped pdb's
monitoring backend on for-over-list with Internal StopIteration.

Assisted-by: Grok:grok-4.6

* Fix generator FOR_ITER events and review issues

Restore settrace StopIteration for generator for-loops so
test_bdb next/return cases pass, while sequence iterators still
skip RAISE.

Honor review notes: addop oparg bounds, linetable locations,
AST instance errors, MRO subclass lock, builtin-dict notify,
direct-base layout flags, and leftover _testinternalcapi args.

Assisted-by: Grok:grok-4.6
2026-09-19 00:40:20 +09:00

44140 lines
1.5 MiB
Plaintext

//!
//! Take an AST and transform it into bytecode
//!
//! Inspirational code:
//! <https://github.com/python/cpython/blob/main/Python/compile.c>
//! <https://github.com/micropython/micropython/blob/master/py/compile.c>
// spell-checker:ignore starunpack subscripter
#![deny(clippy::cast_possible_truncation)]
use crate::{
IndexMap, IndexSet, ToPythonName, ast_constant_value_to_constant_data,
error::{CodegenError, CodegenErrorType, InternalError},
interpolated_string_literal_value, interpolation_debug_text,
ir::{self, Block, BlockIdx, Blocks},
preprocess, string_literal_part_value, string_literal_value,
symboltable::{self, CompilerScope, Symbol, SymbolFlags, SymbolScope, SymbolTable},
unparse::UnparseExpr,
};
use alloc::borrow::Cow;
use core::{mem, slice};
use malachite_bigint::BigInt;
use num_complex::Complex;
use num_traits::{Num, ToPrimitive, Zero};
use ruff_python_ast::{self as ast, name::Name};
use ruff_text_size::{Ranged, TextRange, TextSize, TextSlice};
use rustpython_compiler_core::{
Mode, OneIndexed, PositionEncoding, SourceFile, SourceLocation,
bytecode::{
self, AnyInstruction, AnyOpcode, Arg as OpArgMarker, BinaryOperator, BuildSliceArgCount,
CodeObject, ComparisonOperator, ConstantData, ConvertValueOparg, Instruction,
IntrinsicFunction1, Invert, LoadAttr, LoadSuperAttr, OpArg, OpArgType, PseudoInstruction,
SpecialMethod, UnpackExArgs, oparg,
},
};
use rustpython_literal::{
complex as literal_complex,
escape::{AsciiEscape, UnicodeEscape},
float as literal_float,
};
use rustpython_wtf8::Wtf8Buf;
/// Extension trait for `ast::Expr` to add constant checking methods
trait ExprExt {
/// Returns true if the expression is a constant literal with no side effects.
fn is_constant(&self) -> bool;
}
impl ExprExt for ast::Expr {
fn is_constant(&self) -> bool {
matches!(
self,
Self::NumberLiteral(_)
| Self::StringLiteral(_)
| Self::BytesLiteral(_)
| Self::Constant(_)
| Self::NoneLiteral(_)
| Self::BooleanLiteral(_)
| Self::EllipsisLiteral(_)
)
}
}
const CO_MAXBLOCKS: usize = 21;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FBlockType {
WhileLoop,
ForLoop,
TryExcept,
FinallyTry,
FinallyEnd,
With,
AsyncWith,
HandlerCleanup,
PopValue,
ExceptionHandler,
ExceptionGroupHandler,
AsyncComprehensionGenerator,
StopIteration,
}
/// Stores additional data for fblock unwinding
// fb_datum
#[derive(Debug, Clone)]
pub enum FBlockDatum {
None,
/// For FinallyTry: stores the finally body statements to compile during unwind
FinallyBody(Vec<ast::Stmt>),
/// For HandlerCleanup: stores the exception variable name (e.g., "e" in "except X as e")
ExceptionName(String),
}
/// Type of super() call optimization detected by can_optimize_super_call()
#[derive(Debug, Clone)]
enum SuperCallType<'a> {
/// super(class, self) - explicit 2-argument form
TwoArg {
class_arg: &'a ast::Expr,
self_arg: &'a ast::Expr,
},
/// super() - implicit 0-argument form (uses __class__ cell)
ZeroArg,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BuiltinGeneratorCallKind {
Tuple,
All,
Any,
}
#[derive(Debug, Clone)]
pub struct FBlockInfo {
pub fb_type: FBlockType,
// CPython _PyCompile_FBlockInfo stores jump_target_label values here.
pub(crate) fb_block: ir::InstructionSequenceLabel,
// CPython's optional type-specific exit or cleanup jump_target_label.
pub(crate) fb_exit: ir::InstructionSequenceLabel,
pub fb_range: TextRange,
// additional data for fblock unwinding
pub fb_datum: FBlockDatum,
}
pub(crate) type InternalResult<T> = Result<T, InternalError>;
type CompileResult<T> = Result<T, CodegenError>;
pub type SyntaxWarningHandler<'a> =
dyn FnMut(SourceLocation, String) -> Result<(), CodegenError> + 'a;
fn warn_ast_preprocess_syntax(
source_file: &SourceFile,
handler: &mut SyntaxWarningHandler<'_>,
range: TextRange,
message: String,
) -> CompileResult<()> {
let location = source_file
.to_source_code()
.source_location(range.start(), PositionEncoding::Utf8);
handler(location, message)
}
fn checked_future_features(
ast: &ruff_python_ast::Mod,
source_file: &SourceFile,
) -> CompileResult<bytecode::CodeFlags> {
preprocess::checked_future_features(ast).map_err(|err| {
let source_code = source_file.to_source_code();
let location = source_code.source_location(err.range.start(), PositionEncoding::Utf8);
let end_location = source_code.source_location(err.range.end(), PositionEncoding::Utf8);
let error = match err.kind {
preprocess::FutureFeatureErrorKind::InvalidFeature(feature) => {
CodegenErrorType::InvalidFutureFeature(feature)
}
preprocess::FutureFeatureErrorKind::InvalidBraces => {
CodegenErrorType::InvalidFutureBraces
}
};
CodegenError {
location: Some(location),
end_location: Some(end_location),
error,
source_path: source_file.name().to_owned(),
}
})
}
#[derive(PartialEq, Eq, Clone, Copy)]
enum NameUsage {
Load,
Store,
Delete,
}
/// Main structure holding the state of compilation.
struct Compiler<'a> {
code_stack: Vec<ir::CodeInfo>,
symbol_table_stack: Vec<SymbolTable>,
source_file: SourceFile,
// current_source_location: SourceLocation,
current_source_range: TextRange,
future_features: bytecode::CodeFlags,
future_annotations: bool,
ctx: CompileContext,
opts: CompileOpts,
in_annotation: bool,
/// True when compiling in "single" (interactive) mode.
/// Expression statements at module scope emit CALL_INTRINSIC_1(Print).
interactive: bool,
/// Counter for dead-code elimination during constant folding.
/// When > 0, the compiler walks AST (consuming sub_tables) but emits no bytecode.
/// Mirrors CPython's `c_do_not_emit_bytecode`.
do_not_emit_bytecode: u32,
/// Mirrors `c_disable_warning` while compiling FINALLY_END copies.
disable_warning: u32,
syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>,
/// If true, keep unoptimized nested instruction sequences on the parent.
save_nested_seqs: bool,
}
/// A Python `__future__` feature flag imported via `from __future__ import <feature>`.
///
/// # See Also
///
/// - [Python documentation on `__future__`](https://docs.python.org/3.14/library/__future__.html)
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FutureFeature {
/// ```py
/// from __future__ import absolute_import
/// ```
AbsoluteImport,
/// ```py
/// from __future__ import annotations
/// ```
Annotations,
/// ```py
/// from __future__ import barry_as_FLUFL
/// ```
BarryAsFLUFL,
/// ```py
/// from __future__ import braces
/// ```
Braces,
/// ```py
/// from __future__ import division
/// ```
Division,
/// ```py
/// from __future__ import generator_stop
/// ```
GeneratorStop,
/// ```py
/// from __future__ import generators
/// ```
Generators,
/// ```py
/// from __future__ import nested_scopes
/// ```
NestedScopes,
/// ```py
/// from __future__ import print_function
/// ```
PrintFunction,
/// ```py
/// from __future__ import unicode_literals
/// ```
UnicodeLiterals,
/// ```py
/// from __future__ import with_statement
/// ```
WithStatement,
}
impl TryFrom<&str> for FutureFeature {
type Error = String;
fn try_from(name: &str) -> Result<Self, Self::Error> {
Ok(match name {
"absolute_import" => Self::AbsoluteImport,
"annotations" => Self::Annotations,
"barry_as_FLUFL" => Self::BarryAsFLUFL,
"braces" => Self::Braces,
"division" => Self::Division,
"generator_stop" => Self::GeneratorStop,
"generators" => Self::Generators,
"nested_scopes" => Self::NestedScopes,
"print_function" => Self::PrintFunction,
"unicode_literals" => Self::UnicodeLiterals,
"with_statement" => Self::WithStatement,
_ => return Err(name.into()),
})
}
}
#[derive(Clone, Copy)]
enum ComprehensionSymbolSource {
Child,
Inlined,
}
#[derive(Clone, Copy)]
struct SymbolTableCursors {
sub_table: usize,
hidden_annotation_block: usize,
inlined_comprehension_block: usize,
}
#[derive(Clone, Debug)]
pub struct CompileOpts {
/// How optimized the bytecode output should be; any optimize > 0 does
/// not emit assert statements
pub optimize: u8,
/// Include column info in bytecode (-X no_debug_ranges disables)
pub debug_ranges: bool,
/// Maximum decimal integer literal digits, matching sys.int_info/default.
pub int_max_str_digits: usize,
/// Allow module-level await/async-for/async-with, matching PyCF_ALLOW_TOP_LEVEL_AWAIT.
pub allow_top_level_await: bool,
/// Future compiler flags passed explicitly to compile(), matching cf_flags merge.
pub future_features: bytecode::CodeFlags,
/// Keep single-input blocks incomplete until a terminating newline is seen.
pub dont_imply_dedent: bool,
/// Recursion limit used by compiler tree walks, matching Py_EnterRecursiveCall.
pub recursion_limit: usize,
}
impl Default for CompileOpts {
fn default() -> Self {
Self {
optimize: 0,
debug_ranges: true,
int_max_str_digits: 4300,
allow_top_level_await: false,
future_features: bytecode::CodeFlags::empty(),
dont_imply_dedent: false,
recursion_limit: 1000,
}
}
}
#[derive(Debug, Clone, Copy)]
struct CompileContext {
in_class: bool,
func: FunctionContext,
/// True if we're anywhere inside an async function (even inside nested comprehensions)
in_async_scope: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum FunctionContext {
NoFunction,
Function,
AsyncFunction,
}
impl CompileContext {
fn in_func(self) -> bool {
self.func != FunctionContext::NoFunction
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ComprehensionType {
Generator,
List,
Set,
Dict,
}
#[derive(Debug, Clone, Copy)]
enum ComprehensionLoopControl {
Iteration {
loop_block: BlockIdx,
if_cleanup_block: BlockIdx,
after_block: BlockIdx,
backedge_range: TextRange,
is_async: bool,
end_async_for_target: BlockIdx,
},
IfCleanupOnly {
if_cleanup_block: BlockIdx,
},
}
fn validate_duplicate_params(params: &ast::Parameters) -> Result<(), CodegenErrorType> {
let mut seen_params = IndexSet::default();
for param in params {
let param_name = param.name().as_str();
if !seen_params.insert(param_name) {
return Err(CodegenErrorType::SyntaxError(format!(
r#"Duplicate parameter "{param_name}""#
)));
}
}
Ok(())
}
/// Compile an Mod produced from ruff parser
pub fn compile_top(
ast: ruff_python_ast::Mod,
source_file: SourceFile,
mode: Mode,
opts: CompileOpts,
) -> CompileResult<CodeObject> {
compile_top_with_syntax_warning_handler(ast, source_file, mode, opts, None)
}
pub fn compile_top_with_syntax_warning_handler<'a>(
mut ast: ruff_python_ast::Mod,
source_file: SourceFile,
mode: Mode,
mut opts: CompileOpts,
mut syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>,
) -> CompileResult<CodeObject> {
opts.future_features |= checked_future_features(&ast, &source_file)?;
let future_annotations = opts
.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
if let Some(handler) = syntax_warning_handler.as_deref_mut() {
preprocess::warn_control_flow_in_finally(&ast, |range, message| {
warn_ast_preprocess_syntax(&source_file, handler, range, message)
})?;
}
if matches!(mode, Mode::Single)
&& let ruff_python_ast::Mod::Module(module) = &mut ast
{
preprocess::preprocess_statements(
&mut module.body,
opts.optimize,
future_annotations,
false,
);
} else {
preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false);
}
match ast {
ruff_python_ast::Mod::Module(module) => match mode {
Mode::Exec | Mode::Eval => compile_program_with_syntax_warning_handler(
&module,
source_file,
opts,
syntax_warning_handler,
),
Mode::Single => compile_program_single_with_syntax_warning_handler(
&module,
source_file,
opts,
syntax_warning_handler,
),
Mode::BlockExpr => compile_block_expression_with_syntax_warning_handler(
&module,
source_file,
opts,
syntax_warning_handler,
),
},
ruff_python_ast::Mod::Expression(expr) => compile_expression_with_syntax_warning_handler(
&expr,
source_file,
opts,
syntax_warning_handler,
),
}
}
/// Unoptimized instruction sequence and the metadata needed by optimize_cfg.
pub struct CodegenOutput {
pub seq: ir::InstructionSequence,
pub argcount: u32,
pub posonlyargcount: u32,
pub kwonlyargcount: u32,
pub consts: Vec<ConstantData>,
}
/// compile.c _PyCompile_CodeGen
pub fn compile_codegen(
ast: ruff_python_ast::Mod,
source_file: SourceFile,
mode: Mode,
opts: CompileOpts,
) -> CompileResult<CodegenOutput> {
compile_codegen_with_syntax_warning_handler(ast, source_file, mode, opts, None)
}
fn compile_codegen_with_syntax_warning_handler<'a>(
mut ast: ruff_python_ast::Mod,
source_file: SourceFile,
mode: Mode,
mut opts: CompileOpts,
mut syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>,
) -> CompileResult<CodegenOutput> {
opts.future_features |= checked_future_features(&ast, &source_file)?;
let future_annotations = opts
.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
if let Some(handler) = syntax_warning_handler.as_deref_mut() {
preprocess::warn_control_flow_in_finally(&ast, |range, message| {
warn_ast_preprocess_syntax(&source_file, handler, range, message)
})?;
}
if matches!(mode, Mode::Single)
&& let ruff_python_ast::Mod::Module(module) = &mut ast
{
preprocess::preprocess_statements(
&mut module.body,
opts.optimize,
future_annotations,
false,
);
} else {
preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false);
}
let mut compiler = Compiler::new_with_syntax_warning_handler(
opts,
source_file.clone(),
"<module>",
syntax_warning_handler,
);
compiler.save_nested_seqs = true;
match ast {
ruff_python_ast::Mod::Module(module) => match mode {
Mode::Exec | Mode::Eval => {
let symbol_table = scan_module_symbols(&module, &source_file, &compiler.opts)?;
compiler.compile_program(&module, symbol_table)?;
}
Mode::Single => {
let symbol_table = scan_module_symbols(&module, &source_file, &compiler.opts)?;
compiler.compile_program_single(&module.body, symbol_table)?;
}
Mode::BlockExpr => {
let symbol_table = scan_module_symbols(&module, &source_file, &compiler.opts)?;
compiler.compile_block_expr(&module.body, symbol_table)?;
}
},
ruff_python_ast::Mod::Expression(expr) => {
let symbol_table = scan_expr_symbols(&expr, &source_file, &compiler.opts)?;
compiler.compile_eval(&expr, symbol_table)?;
}
}
let mut unit = compiler
.code_stack
.pop()
.expect("codegen leaves the top-level unit on the stack");
unit.instr_sequence.apply_label_map();
Ok(CodegenOutput {
seq: unit.instr_sequence,
argcount: unit.metadata.argcount,
posonlyargcount: unit.metadata.posonlyargcount,
kwonlyargcount: unit.metadata.kwonlyargcount,
consts: unit.metadata.consts.into_vec(),
})
}
/// Compile a standard Python program to bytecode
pub fn compile_program(
ast: &ast::ModModule,
source_file: SourceFile,
opts: CompileOpts,
) -> CompileResult<CodeObject> {
compile_program_with_syntax_warning_handler(ast, source_file, opts, None)
}
fn scan_module_symbols(
ast: &ast::ModModule,
source_file: &SourceFile,
opts: &CompileOpts,
) -> CompileResult<SymbolTable> {
SymbolTable::scan_program_with_options(
ast,
source_file.clone(),
opts.allow_top_level_await,
opts.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS),
opts.recursion_limit,
)
.map_err(|e| e.into_codegen_error(source_file.name().to_owned()))
}
fn scan_expr_symbols(
ast: &ast::ModExpression,
source_file: &SourceFile,
opts: &CompileOpts,
) -> CompileResult<SymbolTable> {
SymbolTable::scan_expr_with_options(
ast,
source_file.clone(),
opts.allow_top_level_await,
opts.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS),
opts.recursion_limit,
)
.map_err(|e| e.into_codegen_error(source_file.name().to_owned()))
}
fn compile_program_with_syntax_warning_handler<'a>(
ast: &ast::ModModule,
source_file: SourceFile,
opts: CompileOpts,
syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>,
) -> CompileResult<CodeObject> {
let symbol_table = scan_module_symbols(ast, &source_file, &opts)?;
let mut compiler = Compiler::new_with_syntax_warning_handler(
opts,
source_file,
"<module>",
syntax_warning_handler,
);
compiler.compile_program(ast, symbol_table)?;
let code = compiler.exit_scope();
trace!("Compilation completed: {code:?}");
Ok(code)
}
/// Compile a Python program to bytecode for the context of a REPL
pub fn compile_program_single(
ast: &ast::ModModule,
source_file: SourceFile,
opts: CompileOpts,
) -> CompileResult<CodeObject> {
compile_program_single_with_syntax_warning_handler(ast, source_file, opts, None)
}
fn compile_program_single_with_syntax_warning_handler<'a>(
ast: &ast::ModModule,
source_file: SourceFile,
opts: CompileOpts,
syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>,
) -> CompileResult<CodeObject> {
let symbol_table = scan_module_symbols(ast, &source_file, &opts)?;
let mut compiler = Compiler::new_with_syntax_warning_handler(
opts,
source_file,
"<module>",
syntax_warning_handler,
);
compiler.compile_program_single(&ast.body, symbol_table)?;
let code = compiler.exit_scope();
trace!("Compilation completed: {code:?}");
Ok(code)
}
pub fn compile_block_expression(
ast: &ast::ModModule,
source_file: SourceFile,
opts: CompileOpts,
) -> CompileResult<CodeObject> {
compile_block_expression_with_syntax_warning_handler(ast, source_file, opts, None)
}
fn compile_block_expression_with_syntax_warning_handler<'a>(
ast: &ast::ModModule,
source_file: SourceFile,
opts: CompileOpts,
syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>,
) -> CompileResult<CodeObject> {
let symbol_table = scan_module_symbols(ast, &source_file, &opts)?;
let mut compiler = Compiler::new_with_syntax_warning_handler(
opts,
source_file,
"<module>",
syntax_warning_handler,
);
compiler.compile_block_expr(&ast.body, symbol_table)?;
let code = compiler.exit_scope();
trace!("Compilation completed: {code:?}");
Ok(code)
}
pub fn compile_expression(
ast: &ast::ModExpression,
source_file: SourceFile,
opts: CompileOpts,
) -> CompileResult<CodeObject> {
compile_expression_with_syntax_warning_handler(ast, source_file, opts, None)
}
fn compile_expression_with_syntax_warning_handler<'a>(
ast: &ast::ModExpression,
source_file: SourceFile,
opts: CompileOpts,
syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>,
) -> CompileResult<CodeObject> {
let symbol_table = scan_expr_symbols(ast, &source_file, &opts)?;
let mut compiler = Compiler::new_with_syntax_warning_handler(
opts,
source_file,
"<module>",
syntax_warning_handler,
);
compiler.compile_eval(ast, symbol_table)?;
let code = compiler.exit_scope();
Ok(code)
}
macro_rules! emit {
// Struct variant with single identifier (e.g., Foo::A { arg })
($c:expr, $enum:ident :: $op:ident { $arg:ident $(,)? } $(,)?) => {
$c.emit_arg($arg, |x| $enum::$op { $arg: x })
};
// Struct variant with explicit value (e.g., Foo::A { arg: 42 })
($c:expr, $enum:ident :: $op:ident { $arg:ident : $arg_val:expr $(,)? } $(,)?) => {
$c.emit_arg($arg_val, |x| $enum::$op { $arg: x })
};
// Tuple variant (e.g., Foo::B(42)). Should never be reached, here for validation.
($c:expr, $enum:ident :: $op:ident($arg_val:expr $(,)? ) $(,)?) => {
panic!("No instruction should be defined as `Instruction::Foo(value)` use `Instruction::Foo { x: value }` instead")
};
// No-arg variant (e.g., Foo::C)
($c:expr, $enum:ident :: $op:ident $(,)?) => {
$c.emit_no_arg($enum::$op)
};
}
fn eprint_location(zelf: &Compiler<'_>) {
let start = zelf
.source_file
.to_source_code()
.source_location(zelf.current_source_range.start(), PositionEncoding::Utf8);
let end = zelf
.source_file
.to_source_code()
.source_location(zelf.current_source_range.end(), PositionEncoding::Utf8);
eprintln!(
"LOCATION: {} from {}:{} to {}:{}",
zelf.source_file.name(),
start.line,
start.character_offset,
end.line,
end.character_offset
);
}
/// Better traceback for internal error
#[track_caller]
fn unwrap_internal<T>(zelf: &Compiler<'_>, r: InternalResult<T>) -> T {
if let Err(ref r_err) = r {
eprintln!("=== CODEGEN PANIC INFO ===");
eprintln!("This IS an internal error: {r_err}");
eprint_location(zelf);
eprintln!("=== END PANIC INFO ===");
}
r.unwrap()
}
fn compiler_unwrap_option<T>(zelf: &Compiler<'_>, o: Option<T>) -> T {
if o.is_none() {
eprintln!("=== CODEGEN PANIC INFO ===");
eprintln!("This IS an internal error, an option was unwrapped during codegen");
eprint_location(zelf);
eprintln!("=== END PANIC INFO ===");
}
o.unwrap()
}
// fn compiler_result_unwrap<T, E: core::fmt::Debug>(zelf: &Compiler, result: Result<T, E>) -> T {
// if result.is_err() {
// eprintln!("=== CODEGEN PANIC INFO ===");
// eprintln!("This IS an internal error, an result was unwrapped during codegen");
// eprint_location(zelf);
// eprintln!("=== END PANIC INFO ===");
// }
// result.unwrap()
// }
/// The pattern context holds information about captured names and jump targets.
#[derive(Clone)]
pub struct PatternContext {
/// A list of names captured by the pattern.
pub stores: Vec<Name>,
/// If false, then any name captures against our subject will raise.
pub allow_irrefutable: bool,
/// A list of jump target labels used on pattern failure.
pub fail_pop: Vec<BlockIdx>,
/// The number of items on top of the stack that should remain.
pub on_top: usize,
}
impl Default for PatternContext {
fn default() -> Self {
Self::new()
}
}
impl PatternContext {
#[must_use]
pub const fn new() -> Self {
Self {
stores: Vec::new(),
allow_irrefutable: false,
fail_pop: Vec::new(),
on_top: 0,
}
}
#[must_use]
pub fn fail_pop_size(&self) -> usize {
self.fail_pop.len()
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum JumpOp {
Jump,
PopJumpIfFalse,
}
/// Type of collection to build in starunpack_helper
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum CollectionType {
Tuple,
List,
Set,
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum InferredType {
Tuple,
List,
Dict,
Set,
FrozenSet,
Generator,
Function,
Template,
Str,
Bytes,
Int,
Float,
Complex,
Bool,
NoneType,
Ellipsis,
Slice,
}
impl InferredType {
const fn name(self) -> &'static str {
match self {
Self::Tuple => "tuple",
Self::List => "list",
Self::Dict => "dict",
Self::Set => "set",
Self::FrozenSet => "frozenset",
Self::Generator => "generator",
Self::Function => "function",
Self::Template => "string.templatelib.Template",
Self::Str => "str",
Self::Bytes => "bytes",
Self::Int => "int",
Self::Float => "float",
Self::Complex => "complex",
Self::Bool => "bool",
Self::NoneType => "NoneType",
Self::Ellipsis => "ellipsis",
Self::Slice => "slice",
}
}
const fn is_long_subclass(self) -> bool {
matches!(self, Self::Int | Self::Bool)
}
}
const STACK_USE_GUIDELINE: u32 = 30;
impl<'warnings> Compiler<'warnings> {
fn constant_truthiness(constant: &ConstantData) -> bool {
match constant {
ConstantData::Tuple { elements } | ConstantData::Frozenset { elements } => {
!elements.is_empty()
}
ConstantData::Integer { value } => !value.is_zero(),
ConstantData::Float { value } => *value != 0.0,
ConstantData::Complex { value } => value.re != 0.0 || value.im != 0.0,
ConstantData::Boolean { value } => *value,
ConstantData::Str { value } => !value.is_empty(),
ConstantData::Bytes { value } => !value.is_empty(),
ConstantData::Code { .. } | ConstantData::Slice { .. } | ConstantData::Ellipsis => true,
ConstantData::None => false,
}
}
fn infer_type_constant(constant: &ConstantData) -> Option<InferredType> {
match constant {
ConstantData::Tuple { .. } => Some(InferredType::Tuple),
ConstantData::Frozenset { .. } => Some(InferredType::FrozenSet),
ConstantData::Integer { .. } => Some(InferredType::Int),
ConstantData::Float { .. } => Some(InferredType::Float),
ConstantData::Complex { .. } => Some(InferredType::Complex),
ConstantData::Boolean { .. } => Some(InferredType::Bool),
ConstantData::Str { .. } => Some(InferredType::Str),
ConstantData::Bytes { .. } => Some(InferredType::Bytes),
ConstantData::None => Some(InferredType::NoneType),
ConstantData::Ellipsis => Some(InferredType::Ellipsis),
ConstantData::Slice { .. } => Some(InferredType::Slice),
ConstantData::Code { .. } => None,
}
}
fn infer_type(&self, expr: &ast::Expr) -> Option<InferredType> {
if let Some(constant) = self.ast_constant_value(expr) {
return Self::infer_type_constant(&constant);
}
match expr {
ast::Expr::Tuple(_) => Some(InferredType::Tuple),
ast::Expr::List(_) | ast::Expr::ListComp(_) => Some(InferredType::List),
ast::Expr::Dict(_) | ast::Expr::DictComp(_) => Some(InferredType::Dict),
ast::Expr::Set(_) | ast::Expr::SetComp(_) => Some(InferredType::Set),
ast::Expr::Generator(_) => Some(InferredType::Generator),
ast::Expr::Lambda(_) => Some(InferredType::Function),
ast::Expr::TString(_) => Some(InferredType::Template),
ast::Expr::FString(_) | ast::Expr::StringLiteral(_) => Some(InferredType::Str),
ast::Expr::BytesLiteral(_) => Some(InferredType::Bytes),
ast::Expr::NumberLiteral(number) => match number.value {
ast::Number::Int(_) => Some(InferredType::Int),
ast::Number::Float(_) => Some(InferredType::Float),
ast::Number::Complex { .. } => Some(InferredType::Complex),
},
ast::Expr::BooleanLiteral(_) => Some(InferredType::Bool),
ast::Expr::NoneLiteral(_) => Some(InferredType::NoneType),
ast::Expr::EllipsisLiteral(_) => Some(InferredType::Ellipsis),
ast::Expr::Slice(_) => Some(InferredType::Slice),
_ => None,
}
}
fn is_constant_expr(&self, expr: &ast::Expr) -> bool {
if self.ast_constant_value(expr).is_some() {
return true;
}
matches!(
expr,
ast::Expr::StringLiteral(_)
| ast::Expr::BytesLiteral(_)
| ast::Expr::NumberLiteral(_)
| ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
| ast::Expr::EllipsisLiteral(_)
)
}
fn is_constant_slice(&self, slice: &ast::Expr) -> bool {
match slice {
ast::Expr::Slice(s) => {
let lower_const = s.lower.is_none()
|| s.lower.as_deref().is_some_and(|e| self.is_constant_expr(e));
let upper_const = s.upper.is_none()
|| s.upper.as_deref().is_some_and(|e| self.is_constant_expr(e));
let step_const =
s.step.is_none() || s.step.as_deref().is_some_and(|e| self.is_constant_expr(e));
lower_const && upper_const && step_const
}
_ => false,
}
}
fn should_apply_two_element_slice_optimization(&self, slice: &ast::Expr) -> bool {
!self.is_constant_slice(slice) && matches!(slice, ast::Expr::Slice(s) if s.step.is_none())
}
fn check_is_arg(&self, expr: &ast::Expr) -> bool {
if let Some(constant) = self.ast_constant_value(expr) {
return matches!(
constant,
ConstantData::None | ConstantData::Boolean { .. } | ConstantData::Ellipsis
);
}
if let ast::Expr::Tuple(tuple) = expr {
return !tuple.elts.iter().all(|expr| self.is_constant_expr(expr));
}
if !self.is_constant_expr(expr) {
return true;
}
matches!(
expr,
ast::Expr::NoneLiteral(_)
| ast::Expr::BooleanLiteral(_)
| ast::Expr::EllipsisLiteral(_)
)
}
fn warn_syntax(&mut self, range: TextRange, message: String) -> CompileResult<()> {
if self.disable_warning > 0 {
return Ok(());
}
let Some(handler) = self.syntax_warning_handler.as_deref_mut() else {
return Ok(());
};
let location = self
.source_file
.to_source_code()
.source_location(range.start(), PositionEncoding::Utf8);
handler(location, message)
}
fn check_caller(&mut self, func: &ast::Expr) -> CompileResult<()> {
let warns = self.ast_constant_value(func).is_some()
|| matches!(
func,
ast::Expr::StringLiteral(_)
| ast::Expr::BytesLiteral(_)
| ast::Expr::NumberLiteral(_)
| ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
| ast::Expr::EllipsisLiteral(_)
| ast::Expr::Tuple(_)
| ast::Expr::List(_)
| ast::Expr::ListComp(_)
| ast::Expr::Dict(_)
| ast::Expr::DictComp(_)
| ast::Expr::Set(_)
| ast::Expr::SetComp(_)
| ast::Expr::Generator(_)
| ast::Expr::FString(_)
| ast::Expr::TString(_)
);
if warns && let Some(inferred) = self.infer_type(func) {
self.warn_syntax(
func.range(),
format!(
"'{}' object is not callable; perhaps you missed a comma?",
inferred.name()
),
)?;
}
Ok(())
}
fn check_compare(
&mut self,
range: TextRange,
left: &ast::Expr,
ops: &[ast::CmpOp],
comparators: &[ast::Expr],
) -> CompileResult<()> {
let mut left_is_arg = self.check_is_arg(left);
let mut left_expr = left;
for (op, right_expr) in ops.iter().zip(comparators.iter()) {
let right_is_arg = self.check_is_arg(right_expr);
if matches!(op, ast::CmpOp::Is | ast::CmpOp::IsNot) && (!right_is_arg || !left_is_arg) {
let literal = if !left_is_arg { left_expr } else { right_expr };
if let Some(inferred) = self.infer_type(literal) {
let is_op = matches!(op, ast::CmpOp::Is);
let op = if is_op { "\"is\"" } else { "\"is not\"" };
let replacement = if is_op { "==" } else { "!=" };
self.warn_syntax(
range,
format!(
"{op} with '{}' literal. Did you mean \"{replacement}\"?",
inferred.name()
),
)?;
return Ok(());
}
}
left_is_arg = right_is_arg;
left_expr = right_expr;
}
Ok(())
}
fn constant_warns_as_subscripter(constant: &ConstantData) -> bool {
matches!(
constant,
ConstantData::None
| ConstantData::Ellipsis
| ConstantData::Integer { .. }
| ConstantData::Float { .. }
| ConstantData::Complex { .. }
| ConstantData::Boolean { .. }
| ConstantData::Frozenset { .. }
)
}
fn check_subscripter(&mut self, value: &ast::Expr) -> CompileResult<()> {
let warns = self
.ast_constant_value(value)
.is_some_and(|constant| Self::constant_warns_as_subscripter(&constant))
|| matches!(
value,
ast::Expr::NoneLiteral(_)
| ast::Expr::EllipsisLiteral(_)
| ast::Expr::NumberLiteral(_)
| ast::Expr::BooleanLiteral(_)
| ast::Expr::Set(_)
| ast::Expr::SetComp(_)
| ast::Expr::Generator(_)
| ast::Expr::TString(_)
| ast::Expr::Lambda(_)
);
if warns && let Some(inferred) = self.infer_type(value) {
self.warn_syntax(
value.range(),
format!(
"'{}' object is not subscriptable; perhaps you missed a comma?",
inferred.name()
),
)?;
}
Ok(())
}
fn check_index(&mut self, value: &ast::Expr, slice: &ast::Expr) -> CompileResult<()> {
let Some(index_type) = self.infer_type(slice) else {
return Ok(());
};
if index_type.is_long_subclass() || index_type == InferredType::Slice {
return Ok(());
}
let constant_warns = self.ast_constant_value(value).is_some_and(|constant| {
matches!(
constant,
ConstantData::Str { .. } | ConstantData::Bytes { .. } | ConstantData::Tuple { .. }
)
});
let warns = constant_warns
|| matches!(
value,
ast::Expr::StringLiteral(_)
| ast::Expr::BytesLiteral(_)
| ast::Expr::Tuple(_)
| ast::Expr::List(_)
| ast::Expr::ListComp(_)
| ast::Expr::FString(_)
);
if warns && let Some(value_type) = self.infer_type(value) {
self.warn_syntax(
value.range(),
format!(
"{} indices must be integers or slices, not {}; perhaps you missed a comma?",
value_type.name(),
index_type.name()
),
)?;
}
Ok(())
}
fn check_assert(&mut self, assert_stmt: &ast::StmtAssert) -> CompileResult<()> {
let warns = match &*assert_stmt.test {
ast::Expr::Tuple(tuple) => !tuple.elts.is_empty(),
_ => matches!(
self.ast_constant_value(&assert_stmt.test),
Some(ConstantData::Tuple { ref elements }) if !elements.is_empty()
),
};
if warns {
self.warn_syntax(
assert_stmt.range,
"assertion is always true, perhaps remove parentheses?".to_owned(),
)?;
}
Ok(())
}
fn new_with_syntax_warning_handler(
opts: CompileOpts,
source_file: SourceFile,
code_name: &str,
syntax_warning_handler: Option<&'warnings mut SyntaxWarningHandler<'warnings>>,
) -> Self {
let module_code = ir::CodeInfo {
// CPython convention: top-level module / interactive /
// expression code does not carry CO_NEWLOCALS or CO_OPTIMIZED.
// (See `Python/compile.c compiler_enter_scope` for module
// scope.) This matches the per-scope mapping at
// enter_scope::CompilerScope::Module below, which also returns
// empty flags. frame.rs:725-731 then binds locals to globals
// for module/REPL frames whose `scope.locals` is None - the
// correct semantics for `exec(code, globals)` and module init.
flags: bytecode::CodeFlags::empty(),
source_path: source_file.name().to_owned(),
private: None,
blocks: Blocks::from([Block::default()]),
current_block: BlockIdx::new(0),
instr_sequence: ir::InstructionSequence::new(),
instr_sequence_label_map: ir::InstructionSequenceLabelMap::new(),
annotations_instr_sequence: None,
metadata: ir::CodeUnitMetadata {
name: code_name.to_string(),
qualname: Some(code_name.to_string()),
consts: Default::default(),
names: IndexSet::default(),
varnames: IndexSet::default(),
cellvars: IndexSet::default(),
freevars: IndexSet::default(),
fast_hidden: IndexMap::default(),
fast_hidden_final: IndexSet::default(),
argcount: 0,
posonlyargcount: 0,
kwonlyargcount: 0,
firstlineno: OneIndexed::MIN,
},
static_attributes: None,
in_inlined_comp: false,
fblock: Vec::with_capacity(CO_MAXBLOCKS),
symbol_table_index: 0, // Module is always the first symbol table
nparams: 0,
in_conditional_block: 0,
next_conditional_annotation_index: 0,
};
Self {
code_stack: vec![module_code],
symbol_table_stack: Vec::new(),
source_file,
// current_source_location: SourceLocation::default(),
current_source_range: TextRange::default(),
future_features: opts.future_features,
future_annotations: false,
ctx: CompileContext {
in_class: false,
func: FunctionContext::NoFunction,
in_async_scope: false,
},
opts,
in_annotation: false,
interactive: false,
do_not_emit_bytecode: 0,
disable_warning: 0,
syntax_warning_handler,
save_nested_seqs: false,
}
}
fn compile_module_annotation_setup_sequence(
&mut self,
body: &[ast::Stmt],
loc: TextRange,
) -> CompileResult<()> {
let (
saved_blocks,
saved_current_block,
saved_instr_sequence,
saved_instr_sequence_label_map,
saved_annotations_instr_sequence,
) = {
let code = self.current_code_info();
(
mem::replace(&mut code.blocks, Blocks::from([Block::default()])),
mem::replace(&mut code.current_block, BlockIdx::new(0)),
mem::replace(&mut code.instr_sequence, ir::InstructionSequence::new()),
mem::replace(
&mut code.instr_sequence_label_map,
ir::InstructionSequenceLabelMap::new(),
),
code.annotations_instr_sequence.take(),
)
};
let result = self.compile_module_annotate(body, Some(loc));
{
let code = self.current_code_info();
code.blocks = saved_blocks;
let annotations_instr_sequence =
mem::replace(&mut code.instr_sequence, saved_instr_sequence);
code.current_block = saved_current_block;
code.instr_sequence_label_map = saved_instr_sequence_label_map;
debug_assert!(saved_annotations_instr_sequence.is_none());
if matches!(result, Ok(true)) {
code.annotations_instr_sequence = Some(annotations_instr_sequence);
} else {
code.annotations_instr_sequence = saved_annotations_instr_sequence;
}
};
result.map(|_| ())
}
/// Compile just start and stop of a slice (for BINARY_SLICE/STORE_SLICE)
// = codegen_slice_two_parts
fn compile_slice_two_parts(&mut self, s: &ast::ExprSlice) -> CompileResult<()> {
// Compile lower (or None)
if let Some(lower) = &s.lower {
self.compile_expression(lower)?;
} else {
self.set_source_range(s.range);
self.emit_load_const(ConstantData::None);
}
// Compile upper (or None)
if let Some(upper) = &s.upper {
self.compile_expression(upper)?;
} else {
self.set_source_range(s.range);
self.emit_load_const(ConstantData::None);
}
Ok(())
}
/// Compile a subscript expression
// = compiler_subscript
fn compile_subscript(
&mut self,
value: &ast::Expr,
slice: &ast::Expr,
ctx: ast::ExprContext,
) -> CompileResult<()> {
// Save full subscript expression range (set by compile_expression before this call)
let subscript_range = self.current_source_range;
if matches!(ctx, ast::ExprContext::Load) {
self.check_subscripter(value)?;
self.check_index(value, slice)?;
}
// VISIT(c, expr, e->v.Subscript.value)
self.compile_expression(value)?;
// Handle two-element non-constant slice with BINARY_SLICE/STORE_SLICE
let use_slice_opt = matches!(ctx, ast::ExprContext::Load | ast::ExprContext::Store)
&& self.should_apply_two_element_slice_optimization(slice);
if use_slice_opt {
match slice {
ast::Expr::Slice(s) => self.compile_slice_two_parts(s)?,
_ => unreachable!(
"should_use_slice_optimization should only return true for ast::Expr::Slice"
),
};
} else {
// VISIT(c, expr, e->v.Subscript.slice)
self.compile_expression(slice)?;
}
// Restore full subscript expression range before emitting
self.set_source_range(subscript_range);
match (use_slice_opt, ctx) {
(true, ast::ExprContext::Load) => emit!(self, Instruction::BinarySlice),
(true, ast::ExprContext::Store) => emit!(self, Instruction::StoreSlice),
(true, _) => unreachable!(),
(false, ast::ExprContext::Load) => emit!(
self,
Instruction::BinaryOp {
op: BinaryOperator::Subscr
}
),
(false, ast::ExprContext::Store) => emit!(self, Instruction::StoreSubscr),
(false, ast::ExprContext::Del) => emit!(self, Instruction::DeleteSubscr),
(false, ast::ExprContext::Invalid) => {
return Err(self.error(CodegenErrorType::SyntaxError(
"Invalid expression context".to_owned(),
)));
}
}
Ok(())
}
/// Helper function for compiling tuples/lists/sets with starred expressions
///
/// ast::Parameters:
/// - elts: The elements to compile
/// - pushed: Number of items already on the stack
/// - collection_type: What type of collection to build (tuple, list, set)
///
// = starunpack_helper in compile.c
fn starunpack_helper_impl(
&mut self,
elts: &[ast::Expr],
injected_arg: Option<&Name>,
pushed: u32,
collection_type: CollectionType,
) -> CompileResult<()> {
let collection_range = self.current_source_range;
let n = elts.len().to_u32();
let seen_star = elts.iter().any(|e| matches!(e, ast::Expr::Starred(_)));
let injected_count = u32::from(injected_arg.is_some());
let big = n + pushed + injected_count > STACK_USE_GUIDELINE;
// Constant collections are not folded here: the late flowgraph
// optimization passes introduce tuple-backed constants after their
// operands have first been emitted, matching the constant ordering.
// If no stars and not too big, compile all elements and build once
if !seen_star && !big {
for elt in elts {
self.compile_expression(elt)?;
}
if let Some(injected_arg) = injected_arg {
self.set_source_range(collection_range);
self.load_name(injected_arg)?;
}
let total_size = n + injected_count + pushed;
self.set_source_range(collection_range);
match collection_type {
CollectionType::List => {
emit!(self, Instruction::BuildList { count: total_size });
}
CollectionType::Set => {
emit!(self, Instruction::BuildSet { count: total_size });
}
CollectionType::Tuple => {
emit!(self, Instruction::BuildTuple { count: total_size });
}
}
return Ok(());
}
// Has stars or too big: use streaming approach.
let mut sequence_built = false;
let mut i = 0u32;
if big {
self.set_source_range(collection_range);
match collection_type {
CollectionType::List => {
emit!(self, Instruction::BuildList { count: pushed });
sequence_built = true;
}
CollectionType::Set => {
emit!(self, Instruction::BuildSet { count: pushed });
sequence_built = true;
}
CollectionType::Tuple => {
emit!(self, Instruction::BuildList { count: pushed });
sequence_built = true;
}
}
}
for elt in elts {
if let ast::Expr::Starred(ast::ExprStarred { value, .. }) = elt {
// When we hit first star, build sequence with elements so far
if !sequence_built {
self.set_source_range(collection_range);
match collection_type {
CollectionType::List => {
emit!(self, Instruction::BuildList { count: i + pushed });
}
CollectionType::Set => {
emit!(self, Instruction::BuildSet { count: i + pushed });
}
CollectionType::Tuple => {
emit!(self, Instruction::BuildList { count: i + pushed });
}
}
sequence_built = true;
}
// Compile the starred expression and extend
self.compile_expression(value)?;
self.set_source_range(collection_range);
match collection_type {
CollectionType::List => {
emit!(self, Instruction::ListExtend { i: 1 });
}
CollectionType::Set => {
emit!(self, Instruction::SetUpdate { i: 1 });
}
CollectionType::Tuple => {
emit!(self, Instruction::ListExtend { i: 1 });
}
}
} else {
// Non-starred element
self.compile_expression(elt)?;
if sequence_built {
// Sequence already exists, append to it
self.set_source_range(collection_range);
match collection_type {
CollectionType::List => {
emit!(self, Instruction::ListAppend { i: 1 });
}
CollectionType::Set => {
emit!(self, Instruction::SetAdd { i: 1 });
}
CollectionType::Tuple => {
emit!(self, Instruction::ListAppend { i: 1 });
}
}
} else {
// Still collecting elements before first star
i += 1;
}
}
}
debug_assert!(sequence_built);
if let Some(injected_arg) = injected_arg {
self.set_source_range(collection_range);
self.load_name(injected_arg)?;
self.set_source_range(collection_range);
match collection_type {
CollectionType::List | CollectionType::Tuple => {
emit!(self, Instruction::ListAppend { i: 1 });
}
CollectionType::Set => {
emit!(self, Instruction::SetAdd { i: 1 });
}
}
}
if collection_type == CollectionType::Tuple {
self.set_source_range(collection_range);
emit!(
self,
Instruction::CallIntrinsic1 {
func: IntrinsicFunction1::ListToTuple
}
);
}
Ok(())
}
fn starunpack_helper(
&mut self,
elts: &[ast::Expr],
pushed: u32,
collection_type: CollectionType,
) -> CompileResult<()> {
self.starunpack_helper_impl(elts, None, pushed, collection_type)
}
fn error(&mut self, error: CodegenErrorType) -> CodegenError {
self.error_ranged(error, self.current_source_range)
}
fn error_ranged(&mut self, error: CodegenErrorType, range: TextRange) -> CodegenError {
let source_code = self.source_file.to_source_code();
let location = source_code.source_location(range.start(), PositionEncoding::Utf8);
let end_location = source_code.source_location(range.end(), PositionEncoding::Utf8);
CodegenError {
error,
location: Some(location),
end_location: Some(end_location),
source_path: self.source_file.name().to_owned(),
}
}
fn error_optional_range(
&mut self,
error: CodegenErrorType,
range: Option<TextRange>,
) -> CodegenError {
match range {
Some(range) => self.error_ranged(error, range),
None => CodegenError {
error,
location: None,
end_location: None,
source_path: self.source_file.name().to_owned(),
},
}
}
/// Get the SymbolTable for the current scope.
fn current_symbol_table(&self) -> &SymbolTable {
self.symbol_table_stack
.last()
.expect("symbol_table_stack is empty! This is a compiler bug.")
}
fn has_enclosing_non_module_code_scope(&self) -> bool {
self.code_stack.len() > 1
}
/// Match CPython's `is_import_originated()`: only imports recorded in the
/// module-level symbol table suppress method-call optimization.
fn is_name_imported(&self, name: &str) -> bool {
self.symbol_table_stack
.first()
.and_then(|table| table.symbols.get(name))
.is_some_and(|sym| sym.flags.contains(SymbolFlags::DEF_IMPORT))
}
/// Get the cell-relative index of a free variable.
/// Returns ncells + freevar_idx. Fixed up to localsplus index during finalize.
fn get_free_var_index(&mut self, name: &str) -> oparg::VarNum {
let info = self.code_stack.last_mut().unwrap();
let idx = info
.metadata
.freevars
.get_index_of(name)
.unwrap_or_else(|| info.metadata.freevars.insert_full(name.to_owned()).0);
(idx + info.metadata.cellvars.len()).to_u32().into()
}
/// Get the cell-relative index of a cell variable.
/// Returns cellvar_idx. Fixed up to localsplus index during finalize.
fn get_cell_var_index(&mut self, name: &str) -> oparg::VarNum {
let info = self.code_stack.last_mut().unwrap();
let idx = info
.metadata
.cellvars
.get_index_of(name)
.unwrap_or_else(|| info.metadata.cellvars.insert_full(name.to_owned()).0);
idx.to_u32().into()
}
/// Get the index of a local variable.
fn get_local_var_index(&mut self, name: &str) -> oparg::VarNum {
let info = self.code_stack.last_mut().unwrap();
let idx = info
.metadata
.varnames
.get_index_of(name)
.unwrap_or_else(|| info.metadata.varnames.insert_full(name.to_owned()).0);
idx.to_u32().into()
}
/// Get the index of a global name.
fn get_global_name_index(&mut self, name: &str) -> u32 {
let info = self.code_stack.last_mut().unwrap();
let idx = info
.metadata
.names
.get_index_of(name)
.unwrap_or_else(|| info.metadata.names.insert_full(name.to_owned()).0);
idx.to_u32()
}
/// Push the next symbol table on to the stack
fn push_symbol_table(&mut self) -> CompileResult<&SymbolTable> {
// Look up the next table contained in the scope of the current table
let current_table = self
.symbol_table_stack
.last_mut()
.expect("no current symbol table");
if current_table.next_sub_table >= current_table.sub_tables.len() {
let name = current_table.name.clone();
let typ = current_table.typ;
return Err(self.error(CodegenErrorType::SyntaxError(format!(
"no symbol table available in {name} (type: {typ:?})"
))));
}
while current_table.next_sub_table < current_table.sub_tables.len()
&& current_table.sub_tables[current_table.next_sub_table].typ
== CompilerScope::Annotation
{
current_table.next_sub_table += 1;
}
if current_table.next_sub_table >= current_table.sub_tables.len() {
let name = current_table.name.clone();
let typ = current_table.typ;
return Err(self.error(CodegenErrorType::SyntaxError(format!(
"no symbol table available in {name} (type: {typ:?})"
))));
}
let idx = current_table.next_sub_table;
current_table.next_sub_table += 1;
let table = current_table.sub_tables[idx].clone();
// Push the next table onto the stack
self.symbol_table_stack.push(table);
Ok(self.current_symbol_table())
}
fn push_symbol_table_matching(
&mut self,
typ: CompilerScope,
table_name: &str,
) -> CompileResult<&SymbolTable> {
let current_table = self
.symbol_table_stack
.last_mut()
.expect("no current symbol table");
while current_table.next_sub_table < current_table.sub_tables.len()
&& current_table.sub_tables[current_table.next_sub_table].typ
== CompilerScope::Annotation
{
current_table.next_sub_table += 1;
}
let start = current_table.next_sub_table;
let Some(idx) = current_table.sub_tables[start..]
.iter()
.position(|table| table.typ == typ && table.name == table_name)
.map(|idx| start + idx)
else {
let name = current_table.name.clone();
let current_typ = current_table.typ;
return Err(self.error(CodegenErrorType::SyntaxError(format!(
"no matching symbol table {table_name} ({typ:?}) available in {name} (type: {current_typ:?})"
))));
};
let table = current_table.sub_tables[idx].clone();
current_table.next_sub_table = idx + 1;
self.symbol_table_stack.push(table);
Ok(self.current_symbol_table())
}
/// Push the function annotation symbol table.
/// Signature annotation blocks are stored in st_blocks keyed by the
/// arguments AST node. Without future annotations they are also children;
/// with future annotations they are hidden from children and consumed here.
fn push_annotation_symbol_table(&mut self) -> bool {
let Some(annotation_table) = ({
let current_table = self
.symbol_table_stack
.last_mut()
.expect("no current symbol table");
let next_idx = current_table.next_sub_table;
if next_idx < current_table.sub_tables.len()
&& current_table.sub_tables[next_idx].typ == CompilerScope::Annotation
{
let next_table = current_table.sub_tables[next_idx].clone();
current_table.next_sub_table += 1;
Some(next_table)
} else if current_table.next_hidden_annotation_block
< current_table.hidden_annotation_blocks.len()
{
let idx = current_table.next_hidden_annotation_block;
current_table.next_hidden_annotation_block += 1;
Some(current_table.hidden_annotation_blocks[idx].clone())
} else {
None
}
}) else {
return false;
};
self.symbol_table_stack.push(annotation_table);
true
}
fn next_function_annotation_symbol_table_uses_annotations(&self) -> bool {
let current_table = self
.symbol_table_stack
.last()
.expect("no current symbol table");
let next_idx = current_table.next_sub_table;
if next_idx < current_table.sub_tables.len()
&& current_table.sub_tables[next_idx].typ == CompilerScope::Annotation
{
return current_table.sub_tables[next_idx].annotations_used;
}
let hidden_idx = current_table.next_hidden_annotation_block;
current_table
.hidden_annotation_blocks
.get(hidden_idx)
.is_some_and(|table| table.annotations_used)
}
/// Push the annotation symbol table for module/class level annotations
/// This takes annotation_block from the current symbol table (not sub_tables)
fn push_current_annotation_symbol_table(&mut self) -> bool {
let current_table = self
.symbol_table_stack
.last_mut()
.expect("no current symbol table");
// For modules/classes, annotation_block is directly in the current table
if let Some(annotation_block) = current_table.annotation_block.take() {
self.symbol_table_stack.push(*annotation_block);
true
} else {
false
}
}
/// Pop the annotation symbol table.
fn pop_annotation_symbol_table(&mut self) {
self.symbol_table_stack.pop().expect("compiler bug");
}
/// Pop the current symbol table off the stack
fn pop_symbol_table(&mut self) -> SymbolTable {
self.symbol_table_stack.pop().expect("compiler bug")
}
/// Check if a super() call can be optimized
/// Returns Some(SuperCallType) if optimization is possible, None otherwise
fn can_optimize_super_call<'a>(
&self,
value: &'a ast::Expr,
attr: &str,
) -> Option<SuperCallType<'a>> {
// 1. value must be a Call expression
let ast::Expr::Call(ast::ExprCall {
func, arguments, ..
}) = value
else {
return None;
};
// 2. func must be Name("super")
let ast::Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
return None;
};
if id.as_str() != "super" {
return None;
}
// 3. attr must not be "__class__"
if attr == "__class__" {
return None;
}
// 4. No keyword arguments
if !arguments.keywords.is_empty() {
return None;
}
// 5. "super" must be GlobalImplicit in the current scope.
let table = self.current_symbol_table();
if let Some(symbol) = table.lookup(&"super".into())
&& symbol.scope != SymbolScope::GlobalImplicit
{
return None;
}
// Then check the top-level scope and reject any statically
// visible symbol for "super", not just local bindings.
if let Some(top_table) = self.symbol_table_stack.first()
&& top_table.lookup(&"super".into()).is_some()
{
return None;
}
// 6. Check argument pattern
let args = &arguments.args;
// No starred expressions allowed
if args.iter().any(|arg| matches!(arg, ast::Expr::Starred(_))) {
return None;
}
match args.len() {
2 => {
// 2-arg: super(class, self)
Some(SuperCallType::TwoArg {
class_arg: &args[0],
self_arg: &args[1],
})
}
0 => {
// 0-arg: super() - need __class__ cell and first parameter
// Enclosing function should have at least one positional argument
let info = self.code_stack.last()?;
if info.metadata.argcount == 0 && info.metadata.posonlyargcount == 0 {
return None;
}
// Check if __class__ is available as a cell/free variable
// The scope must be Free (from enclosing class) or have DEF_FREE_CLASS flag
{
let symbol = table.lookup(&"__class__".into())?;
if symbol.scope != SymbolScope::Free
&& !symbol.flags.contains(SymbolFlags::DEF_FREE_CLASS)
{
return None;
}
}
Some(SuperCallType::ZeroArg)
}
_ => None, // 1 or 3+ args - not optimizable
}
}
/// Load arguments for super() optimization onto the stack
/// Stack result: [global_super, class, self]
fn load_args_for_super(
&mut self,
super_type: &SuperCallType<'_>,
super_name_range: TextRange,
super_call_range: TextRange,
) -> CompileResult<()> {
// 1. Load global super
self.set_source_range(super_name_range);
self.compile_name(&"super".into(), NameUsage::Load)?;
match super_type {
SuperCallType::TwoArg {
class_arg,
self_arg,
} => {
// 2-arg: load provided arguments
self.compile_expression(class_arg)?;
self.compile_expression(self_arg)?;
}
SuperCallType::ZeroArg => {
// 0-arg: load __class__ cell and first parameter
// Load __class__ from cell/free variable
self.set_source_range(super_call_range);
let scope = self
.get_ref_type(&"__class__".into())
.map_err(|e| self.error(e))?;
let idx = match scope {
SymbolScope::Cell => self.get_cell_var_index("__class__"),
SymbolScope::Free => self.get_free_var_index("__class__"),
_ => {
return Err(self.error(CodegenErrorType::SyntaxError(
"super(): __class__ cell not found".to_owned(),
)));
}
};
emit!(self, Instruction::LoadDeref { i: idx });
// Load first parameter (typically 'self').
// Safety: can_optimize_super_call() ensures argcount > 0, and
// parameters are always added to varnames first (see symboltable.rs).
let first_param = {
let info = self.code_stack.last().unwrap();
info.metadata.varnames.first().cloned()
};
let first_param = first_param.ok_or_else(|| {
self.error(CodegenErrorType::SyntaxError(
"super(): no arguments and no first parameter".to_owned(),
))
})?;
self.set_source_range(super_call_range);
self.compile_name(&first_param.into(), NameUsage::Load)?;
}
}
Ok(())
}
/// Check if this is an inlined comprehension context (PEP 709).
/// Generator expressions are never inlined.
fn is_inlined_comprehension_context(
&self,
comprehension_type: ComprehensionType,
comp_table: &SymbolTable,
) -> bool {
if comprehension_type == ComprehensionType::Generator {
return false;
}
comp_table.comp_inlined
}
/// Enter a new scope
// = compiler_enter_scope
fn enter_scope(
&mut self,
name: &str,
scope_type: CompilerScope,
key: usize, // Symbol table stack index used like CPython's scope key.
lineno: u32,
) -> CompileResult<()> {
// Allocate a new compiler unit
// In Rust, we'll create the structure directly
let source_path = self.source_file.name().to_owned();
// Lookup symbol table entry using key (_PySymtable_Lookup)
let ste = match self.symbol_table_stack.get(key) {
Some(v) => v,
None => {
return Err(self.error(CodegenErrorType::SyntaxError(
"unknown symbol table entry".to_owned(),
)));
}
};
// Use varnames from symbol table (already collected in definition order)
let varname_cache: IndexSet<Name> = ste.varnames.iter().cloned().collect();
let nparams = ste.varnames.len();
// Build cellvars using dictbytype (CELL scope or COMP_CELL flag, sorted)
let mut cellvar_cache = IndexSet::default();
let mut cell_names: Vec<_> = ste
.symbols
.iter()
.filter(|(_, s)| {
s.scope == SymbolScope::Cell || s.flags.contains(SymbolFlags::DEF_COMP_CELL)
})
.map(|(name, _)| name.clone())
.collect();
cell_names.sort();
for name in cell_names {
cellvar_cache.insert(name.into());
}
// Handle implicit __class__ cell if needed
if ste.needs_class_closure {
// Cook up an implicit __class__ cell
debug_assert_eq!(scope_type, CompilerScope::Class);
cellvar_cache.insert("__class__".to_string());
}
// Handle implicit __classdict__ cell if needed
if ste.needs_classdict {
// Cook up an implicit __classdict__ cell
debug_assert_eq!(scope_type, CompilerScope::Class);
cellvar_cache.insert("__classdict__".to_string());
}
// Handle implicit __conditional_annotations__ cell if needed.
if Self::scope_needs_conditional_annotations_cell(ste) {
cellvar_cache.insert("__conditional_annotations__".to_string());
}
// Build freevars using dictbytype (FREE scope, offset by cellvars size)
let mut freevar_cache = IndexSet::default();
let annotation_free_names: IndexSet<Name> = ste
.annotation_block
.as_ref()
.map(|annotation| {
annotation
.symbols
.iter()
.filter(|(_, s)| s.scope == SymbolScope::Free)
.map(|(name, _)| name.clone())
.collect()
})
.unwrap_or_default();
let mut free_names: Vec<_> = ste
.symbols
.iter()
.filter(|(_, s)| {
s.scope == SymbolScope::Free
|| (scope_type != CompilerScope::Class
&& s.flags.contains(SymbolFlags::DEF_FREE_CLASS))
|| (scope_type == CompilerScope::Class
&& s.flags.contains(SymbolFlags::DEF_FREE_CLASS)
&& self.has_enclosing_non_module_code_scope())
})
.filter(|(name, symbol)| {
if !matches!(
scope_type,
CompilerScope::Function | CompilerScope::AsyncFunction | CompilerScope::Lambda
) {
return true;
}
!(annotation_free_names.contains(*name) && symbol.flags.is_empty())
})
.map(|(name, _)| name.clone())
.collect();
free_names.sort();
for name in free_names {
freevar_cache.insert(name.into());
}
// The `__conditional_annotations__` cell an annotation scope reads is
// cooked up here rather than carried by the symbol table, so it lands
// after the names the symbol table did supply.
if scope_type == CompilerScope::Annotation && ste.has_conditional_annotations {
freevar_cache.insert("__conditional_annotations__".to_string());
}
// Initialize u_metadata fields
let (mut flags, posonlyarg_count, arg_count, kwonlyarg_count) = match scope_type {
CompilerScope::Module => (bytecode::CodeFlags::empty(), 0, 0, 0),
CompilerScope::Class => (bytecode::CodeFlags::empty(), 0, 0, 0),
CompilerScope::Function | CompilerScope::AsyncFunction | CompilerScope::Lambda => (
bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED,
0, // Will be set later in enter_function
0, // Will be set later in enter_function
0, // Will be set later in enter_function
),
CompilerScope::Comprehension => (
bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED,
0,
1, // comprehensions take one argument (.0)
0,
),
CompilerScope::TypeParams => (
bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED,
0,
0,
0,
),
CompilerScope::Annotation => (
bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED,
1, // format is positional-only
0,
0,
),
CompilerScope::TypeAlias | CompilerScope::TypeVariable => (
bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED,
1, // format is positional-only
0,
0,
),
};
if ste.is_method {
flags |= bytecode::CodeFlags::METHOD;
}
// CPython sets CO_NESTED from symtable's ste_nested, not merely
// from lexical depth: module-level class methods are CO_METHOD but
// not CO_NESTED.
let mut flags = if ste.is_nested
&& matches!(
scope_type,
CompilerScope::Function
| CompilerScope::AsyncFunction
| CompilerScope::Lambda
| CompilerScope::Comprehension
| CompilerScope::Annotation
| CompilerScope::TypeAlias
| CompilerScope::TypeVariable
| CompilerScope::TypeParams
) {
flags | bytecode::CodeFlags::NESTED
} else {
flags
};
flags |= self.future_features;
// Get private name from parent scope
let private = if !self.code_stack.is_empty() {
self.code_stack.last().unwrap().private.clone()
} else {
None
};
// Create the new compilation unit
let code_info = ir::CodeInfo {
flags,
source_path,
private,
blocks: Blocks::from([Block::default()]),
current_block: BlockIdx::new(0),
instr_sequence: ir::InstructionSequence::new(),
instr_sequence_label_map: ir::InstructionSequenceLabelMap::new(),
annotations_instr_sequence: None,
metadata: ir::CodeUnitMetadata {
name: name.to_owned(),
qualname: None, // Will be set below
consts: Default::default(),
names: IndexSet::default(),
varnames: varname_cache.into_iter().map(Into::into).collect(),
cellvars: cellvar_cache,
freevars: freevar_cache,
fast_hidden: IndexMap::default(),
fast_hidden_final: IndexSet::default(),
argcount: arg_count,
posonlyargcount: posonlyarg_count,
kwonlyargcount: kwonlyarg_count,
firstlineno: OneIndexed::new(lineno as usize).unwrap_or(OneIndexed::MIN),
},
static_attributes: if scope_type == CompilerScope::Class {
Some(IndexSet::default())
} else {
None
},
in_inlined_comp: false,
fblock: Vec::with_capacity(CO_MAXBLOCKS),
symbol_table_index: key,
nparams,
in_conditional_block: 0,
next_conditional_annotation_index: 0,
};
// Push the old compiler unit on the stack (like PyCapsule)
// This happens before setting qualname
self.code_stack.push(code_info);
// Set qualname after pushing (uses compiler_set_qualname logic)
if scope_type != CompilerScope::Module {
self.set_qualname();
}
// Emit RESUME (handles async preamble and module lineno 0)
// CPython: LOCATION(lineno, lineno, 0, 0), then loc.lineno = 0 for module
self.emit_resume_for_scope(scope_type, lineno);
Ok(())
}
/// Emit RESUME instruction with proper handling for module lineno.
/// codegen_enter_scope equivalent for RESUME emission.
fn emit_resume_for_scope(&mut self, scope_type: CompilerScope, lineno: u32) {
// CPython: LOCATION(lineno, lineno, 0, 0)
// Module scope: loc.lineno = 0 (before the first line)
let lineno_override = if scope_type == CompilerScope::Module {
Some(0)
} else {
None
};
// Use lineno for location (col = 0 as in CPython)
let location = SourceLocation {
line: OneIndexed::new(lineno as usize).unwrap_or(OneIndexed::MIN),
character_offset: OneIndexed::MIN, // col = 0
};
let end_location = location; // end_lineno = lineno, end_col = 0
let except_handler = None;
self.cpython_cfg_builder_addop(ir::InstructionInfo {
instr: Instruction::Resume {
context: OpArgMarker::marker(),
}
.into(),
arg: OpArg::new(oparg::ResumeLocation::AtFuncStart.into()),
target: BlockIdx::NULL,
location,
end_location,
except_handler,
lineno_override,
});
}
fn push_output(
&mut self,
flags: bytecode::CodeFlags,
posonlyarg_count: u32,
arg_count: u32,
kwonlyarg_count: u32,
obj_name: &str,
) -> CompileResult<()> {
// First push the symbol table
let table = self.push_symbol_table()?;
let scope_type = table.typ;
// The key is the current position in the symbol table stack
let key = self.symbol_table_stack.len() - 1;
// Get the line number
let lineno = self.get_source_line_number().get();
// Call enter_scope which does most of the work
self.enter_scope(obj_name, scope_type, key, lineno.to_u32())?;
// Override the values that push_output sets explicitly
// enter_scope sets default values based on scope_type, but push_output
// allows callers to specify exact values
if let Some(info) = self.code_stack.last_mut() {
// Preserve flags computed from the symbol-table context.
info.flags = flags
| (info.flags
& (bytecode::CodeFlags::NESTED
| bytecode::CodeFlags::METHOD
| bytecode::CodeFlags::FUTURE_DIVISION
| bytecode::CodeFlags::FUTURE_ABSOLUTE_IMPORT
| bytecode::CodeFlags::FUTURE_WITH_STATEMENT
| bytecode::CodeFlags::FUTURE_PRINT_FUNCTION
| bytecode::CodeFlags::FUTURE_UNICODE_LITERALS
| bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL
| bytecode::CodeFlags::FUTURE_GENERATOR_STOP
| bytecode::CodeFlags::FUTURE_ANNOTATIONS));
info.metadata.argcount = arg_count;
info.metadata.posonlyargcount = posonlyarg_count;
info.metadata.kwonlyargcount = kwonlyarg_count;
}
Ok(())
}
// compiler_exit_scope
fn exit_scope(&mut self) -> CodeObject {
self.pop_symbol_table();
// Various scopes can have sub_tables:
// - ast::TypeParams scope can have sub_tables (the function body's symbol table)
// - Module scope can have sub_tables (for TypeAlias scopes, nested functions, classes)
// - Function scope can have sub_tables (for nested functions, classes)
// - Class scope can have sub_tables (for nested classes, methods)
let pop = self.code_stack.pop();
let stack_top = compiler_unwrap_option(self, pop);
let nested = self
.save_nested_seqs
.then(|| stack_top.instr_sequence.clone());
let code = unwrap_internal(self, stack_top.finalize_code(&self.opts));
if let (Some(nested), Some(parent)) = (nested, self.code_stack.last_mut()) {
parent.instr_sequence.add_nested(nested);
}
code
}
fn expose_annotation_format_parameter(code: &mut CodeObject) {
if let Some(first) = code.varnames.first_mut() {
*first = String::from("format");
}
}
fn configure_annotation_format_parameter(&mut self) {
let info = self.current_code_info();
info.metadata.varnames.insert(".format".to_owned());
info.nparams = 1;
}
/// Exit a function signature annotation scope.
fn exit_annotation_scope(&mut self, saved_ctx: CompileContext) -> CodeObject {
self.pop_annotation_symbol_table();
self.ctx = saved_ctx;
let pop = self.code_stack.pop();
let stack_top = compiler_unwrap_option(self, pop);
let nested = self
.save_nested_seqs
.then(|| stack_top.instr_sequence.clone());
let mut code = unwrap_internal(self, stack_top.finalize_code(&self.opts));
if let (Some(nested), Some(parent)) = (nested, self.code_stack.last_mut()) {
parent.instr_sequence.add_nested(nested);
}
Self::expose_annotation_format_parameter(&mut code);
code
}
/// Enter a function signature annotation scope.
/// Returns None if no matching annotation symbol table exists.
/// On success, returns the saved CompileContext to pass to exit_annotation_scope.
fn enter_annotation_scope(
&mut self,
func_name: &str,
loc: TextRange,
) -> CompileResult<Option<CompileContext>> {
if !self.push_annotation_symbol_table() {
return Ok(None);
}
// Annotation scopes are never async (even inside async functions)
let saved_ctx = self.ctx;
self.ctx = CompileContext {
in_class: saved_ctx.in_class,
func: FunctionContext::Function,
in_async_scope: false,
};
self.set_source_range(loc);
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get();
self.enter_scope(
"__annotate__",
CompilerScope::Annotation,
key,
lineno.to_u32(),
)?;
// enter_scope() qualified the scope by the enclosing scope only; redo it
// now that the annotated function is known. Only signature annotations
// get this treatment - deferred class and module annotations are
// compiled inside the scope they belong to and are already qualified.
self.set_annotation_qualname(func_name);
// Keep the internal ".format" name; exit_annotation_scope()
// renames it to "format" on the final code object.
self.configure_annotation_format_parameter();
// Emit format validation: if format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError
// VALUE_WITH_FAKE_GLOBALS = 2 (from annotationlib.Format)
self.emit_format_validation();
Ok(Some(saved_ctx))
}
/// Emit format parameter validation for annotation scope
/// if format > VALUE_WITH_FAKE_GLOBALS (2): raise NotImplementedError
fn emit_format_validation(&mut self) {
// Load format parameter (first local variable, index 0)
emit!(
self,
Instruction::LoadFast {
var_num: oparg::VarNum::from_u32(0)
}
);
// Load VALUE_WITH_FAKE_GLOBALS constant (2)
self.emit_load_const(ConstantData::Integer { value: 2.into() });
// Compare: format > 2
emit!(
self,
Instruction::CompareOp {
opname: ComparisonOperator::Greater
}
);
// Jump to body if format <= 2 (comparison is false)
let body_block = self.new_block();
emit!(self, Instruction::PopJumpIfFalse { delta: body_block });
// Raise NotImplementedError
emit!(
self,
Instruction::LoadCommonConstant {
idx: bytecode::CommonConstant::NotImplementedError
}
);
emit!(
self,
Instruction::RaiseVarargs {
argc: bytecode::RaiseKind::Raise
}
);
// Body label - continue with annotation evaluation
self.use_cpython_label_block(body_block);
}
/// CPython `_PyCompile_PushFBlock()`: store the active label targets on the
/// fblock stack.
fn push_fblock_labels(
&mut self,
fb_type: FBlockType,
fb_block: ir::InstructionSequenceLabel,
fb_exit: ir::InstructionSequenceLabel,
fb_datum: FBlockDatum,
) -> CompileResult<()> {
let fb_range = self.current_source_range;
if self.current_code_info().fblock.len() >= CO_MAXBLOCKS {
return Err(self.error(CodegenErrorType::SyntaxError(
"too many statically nested blocks".to_owned(),
)));
}
if matches!(fb_type, FBlockType::FinallyEnd) {
self.disable_warning += 1;
}
let code = self.current_code_info();
code.fblock.push(FBlockInfo {
fb_type,
fb_block,
fb_exit,
fb_range,
fb_datum,
});
Ok(())
}
/// CPython `_PyCompile_PopFBlock()`: assert the popped type and label.
fn pop_fblock_label(
&mut self,
expected_type: FBlockType,
expected_block: ir::InstructionSequenceLabel,
) -> FBlockInfo {
let fblock = {
let code = self.current_code_info();
code.fblock.pop().expect("fblock stack underflow")
};
debug_assert_eq!(fblock.fb_type, expected_type);
debug_assert_eq!(
fblock.fb_block, expected_block,
"CPython _PyCompile_PopFBlock asserts the popped fb_block label"
);
if matches!(expected_type, FBlockType::FinallyEnd) {
self.disable_warning -= 1;
}
fblock
}
/// `_PyCompile_PushFBlock()` call used by
/// `codegen_unwind_fblock_stack()` to restore the copied fblock after
/// recursive unwinding.
fn restore_fblock_info(&mut self, fblock: FBlockInfo) -> CompileResult<()> {
let FBlockInfo {
fb_type,
fb_block,
fb_exit,
fb_range,
fb_datum,
} = fblock;
let code = self.current_code_info();
if code.fblock.len() >= CO_MAXBLOCKS {
return Err(self.error_ranged(
CodegenErrorType::SyntaxError("too many statically nested blocks".to_owned()),
fb_range,
));
}
code.fblock.push(FBlockInfo {
fb_type,
fb_block,
fb_exit,
fb_range,
fb_datum,
});
Ok(())
}
fn set_unwind_source_range(&mut self, loc: Option<TextRange>) {
if let Some(range) = loc {
self.set_source_range(range);
}
}
fn mark_unwind_no_location(&mut self, loc: Option<TextRange>) {
if loc.is_none() {
self.set_no_location();
}
}
/// Unwind a single fblock, emitting cleanup code
/// preserve_tos: if true, preserve the top of stack (e.g., return value)
fn unwind_fblock(
&mut self,
info: &FBlockInfo,
preserve_tos: bool,
loc: &mut Option<TextRange>,
) -> CompileResult<()> {
match info.fb_type {
FBlockType::WhileLoop
| FBlockType::ExceptionHandler
| FBlockType::ExceptionGroupHandler
| FBlockType::AsyncComprehensionGenerator
| FBlockType::StopIteration => {
// No cleanup needed
}
FBlockType::ForLoop => {
// When returning from a for-loop, CPython swaps the preserved
// value with the iterator and uses POP_TOP for loop cleanup.
if preserve_tos {
self.set_unwind_source_range(*loc);
emit!(self, Instruction::Swap { i: 2 });
self.mark_unwind_no_location(*loc);
}
self.set_unwind_source_range(*loc);
emit!(self, Instruction::PopTop);
self.mark_unwind_no_location(*loc);
}
FBlockType::TryExcept => {
self.set_unwind_source_range(*loc);
emit!(self, PseudoInstruction::PopBlock);
self.mark_unwind_no_location(*loc);
}
FBlockType::FinallyTry => {
// codegen_unwind_fblock(FINALLY_TRY)
self.set_unwind_source_range(*loc);
emit!(self, PseudoInstruction::PopBlock);
self.mark_unwind_no_location(*loc);
if preserve_tos {
self.push_fblock_labels(
FBlockType::PopValue,
ir::InstructionSequenceLabel::NO_LABEL,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)?;
}
if let FBlockDatum::FinallyBody(ref body) = info.fb_datum {
// This is an extra copy of the finally body, emitted for the
// path that leaves the try block early. Nested scopes are
// handed out by position, and the cursors are still inside
// the try block's own run of scopes here, so seek them to the
// ones this body opened before compiling it. The try
// statement emits its own copies from the same place
// afterwards, so put the cursors back when the copy is done.
let symbol_table_cursors = self.current_symbol_table_cursors();
if let Some(first) = body.first() {
let line = self
.source_file
.to_source_code()
.line_index(first.range().start())
.get()
.to_u32();
self.seek_symbol_table_cursors_to_line(line);
}
self.compile_statements(body)?;
self.set_symbol_table_cursors(symbol_table_cursors);
}
if preserve_tos {
self.pop_fblock_label(
FBlockType::PopValue,
ir::InstructionSequenceLabel::NO_LABEL,
);
}
*loc = None;
}
FBlockType::FinallyEnd => {
// codegen_unwind_fblock(FINALLY_END)
if preserve_tos {
self.set_unwind_source_range(*loc);
emit!(self, Instruction::Swap { i: 2 });
self.mark_unwind_no_location(*loc);
}
self.set_unwind_source_range(*loc);
emit!(self, Instruction::PopTop); // exc_value
self.mark_unwind_no_location(*loc);
if preserve_tos {
self.set_unwind_source_range(*loc);
emit!(self, Instruction::Swap { i: 2 });
self.mark_unwind_no_location(*loc);
}
self.set_unwind_source_range(*loc);
emit!(self, PseudoInstruction::PopBlock);
self.mark_unwind_no_location(*loc);
self.set_unwind_source_range(*loc);
emit!(self, Instruction::PopExcept);
self.mark_unwind_no_location(*loc);
}
FBlockType::With | FBlockType::AsyncWith => {
// Stack: [..., exit_func, self_exit, return_value (if preserve_tos)]
// CPython codegen_unwind_fblock() assigns *ploc = info->fb_loc
// for WITH/ASYNC_WITH cleanup and then makes following unwind
// instructions artificial with *ploc = NO_LOCATION.
*loc = Some(info.fb_range);
self.set_unwind_source_range(*loc);
emit!(self, PseudoInstruction::PopBlock);
if preserve_tos {
// Rotate return value below the exit pair
// [exit_func, self_exit, value] → [value, exit_func, self_exit]
self.set_unwind_source_range(*loc);
emit!(self, Instruction::Swap { i: 3 }); // [value, self_exit, exit_func]
self.set_unwind_source_range(*loc);
emit!(self, Instruction::Swap { i: 2 }); // [value, exit_func, self_exit]
}
self.set_unwind_source_range(*loc);
self.compile_call_exit_with_nones();
// For async with, await the result
if matches!(info.fb_type, FBlockType::AsyncWith) {
self.set_unwind_source_range(*loc);
emit!(self, Instruction::GetAwaitable { r#where: 2 });
self.set_unwind_source_range(*loc);
self.emit_load_const(ConstantData::None);
let _ = self.compile_yield_from_sequence(true);
}
// Pop the __exit__ result
self.set_unwind_source_range(*loc);
emit!(self, Instruction::PopTop);
*loc = None;
}
FBlockType::HandlerCleanup => {
// codegen_unwind_fblock(HANDLER_CLEANUP)
if let FBlockDatum::ExceptionName(_) = info.fb_datum {
// Named handler: PopBlock for inner SETUP_CLEANUP
self.set_unwind_source_range(*loc);
emit!(self, PseudoInstruction::PopBlock);
self.mark_unwind_no_location(*loc);
}
if preserve_tos {
self.set_unwind_source_range(*loc);
emit!(self, Instruction::Swap { i: 2 });
self.mark_unwind_no_location(*loc);
}
// PopBlock for outer SETUP_CLEANUP (ExceptionHandler)
self.set_unwind_source_range(*loc);
emit!(self, PseudoInstruction::PopBlock);
self.mark_unwind_no_location(*loc);
self.set_unwind_source_range(*loc);
emit!(self, Instruction::PopExcept);
self.mark_unwind_no_location(*loc);
// If there's an exception name, clean it up
if let FBlockDatum::ExceptionName(ref name) = info.fb_datum {
self.set_unwind_source_range(*loc);
self.emit_load_const(ConstantData::None);
self.mark_unwind_no_location(*loc);
self.set_unwind_source_range(*loc);
self.store_name(&name.into())?;
self.mark_unwind_no_location(*loc);
self.set_unwind_source_range(*loc);
self.compile_name(&name.into(), NameUsage::Delete)?;
self.mark_unwind_no_location(*loc);
}
}
FBlockType::PopValue => {
if preserve_tos {
self.set_unwind_source_range(*loc);
emit!(self, Instruction::Swap { i: 2 });
self.mark_unwind_no_location(*loc);
}
self.set_unwind_source_range(*loc);
emit!(self, Instruction::PopTop);
self.mark_unwind_no_location(*loc);
}
}
Ok(())
}
/// Unwind the fblock stack, emitting cleanup code for each block
/// preserve_tos: if true, preserve the top of stack (e.g., return value)
/// stop_at_loop: if true, stop when encountering a loop (for break/continue)
fn unwind_fblock_stack(
&mut self,
preserve_tos: bool,
stop_at_loop: bool,
) -> CompileResult<Option<TextRange>> {
let (unwind_loc, _loop_fblock) =
self.unwind_fblock_stack_with_loop(preserve_tos, stop_at_loop)?;
Ok(unwind_loc)
}
/// CPython `codegen_unwind_fblock_stack()`: unwind frame blocks and, when
/// requested by break/continue codegen, return the first loop fblock instead
/// of unwinding it.
fn unwind_fblock_stack_with_loop(
&mut self,
preserve_tos: bool,
stop_at_loop: bool,
) -> CompileResult<(Option<TextRange>, Option<FBlockInfo>)> {
let mut unwind_loc = Some(self.current_source_range);
let loop_fblock =
self.unwind_fblock_stack_inner(preserve_tos, stop_at_loop, &mut unwind_loc)?;
Ok((unwind_loc, loop_fblock))
}
fn unwind_fblock_stack_inner(
&mut self,
preserve_tos: bool,
stop_at_loop: bool,
unwind_loc: &mut Option<TextRange>,
) -> CompileResult<Option<FBlockInfo>> {
let Some(top) = self.current_code_info().fblock.last().cloned() else {
return Ok(None);
};
if matches!(top.fb_type, FBlockType::ExceptionGroupHandler) {
return Err(self.error_optional_range(
CodegenErrorType::BreakContinueReturnInExceptStar,
*unwind_loc,
));
}
if stop_at_loop && matches!(top.fb_type, FBlockType::WhileLoop | FBlockType::ForLoop) {
return Ok(Some(top));
}
let copy = self
.current_code_info()
.fblock
.pop()
.expect("fblock stack underflow");
self.unwind_fblock(&copy, preserve_tos, unwind_loc)?;
let loop_fblock = self.unwind_fblock_stack_inner(preserve_tos, stop_at_loop, unwind_loc)?;
self.restore_fblock_info(copy)?;
Ok(loop_fblock)
}
// could take impl Into<Cow<str>>, but everything is borrowed from ast structs; we never
// actually have a `String` to pass
fn name(&mut self, name: &str) -> bytecode::NameIdx {
self._name_inner(name, |i| &mut i.metadata.names)
}
fn varname(&mut self, name: &str) -> oparg::VarNum {
// NOTE: __debug__ checks are now handled in symboltable phase
oparg::VarNum::from_u32(self._name_inner(name, |i| &mut i.metadata.varnames))
}
fn _name_inner(
&mut self,
name: &str,
cache: impl FnOnce(&mut ir::CodeInfo) -> &mut IndexSet<String>,
) -> u32 {
let target = name.into();
let name = self.mangle(&target);
let cache = cache(self.current_code_info());
cache
.get_index_of(name.as_str())
.unwrap_or_else(|| cache.insert_full(name.to_string()).0)
.to_u32()
}
/// Set the qualified name for the current code object
// = compiler_set_qualname
fn set_qualname(&mut self) -> String {
self.set_qualname_for_function(None)
}
/// Set the qualname of an annotation scope, qualified by the function whose
/// signature it annotates. The annotation block's symbol table entry records
/// that name (`ste_function_name`) and folds it into the qualname, so `f`'s
/// annotation scope is named `f.__annotate__`.
fn set_annotation_qualname(&mut self, function_name: &str) {
self.set_qualname_for_function(Some(function_name));
}
fn set_qualname_for_function(&mut self, function_name: Option<&str>) -> String {
let qualname = self.make_qualname(function_name);
self.current_code_info().metadata.qualname = Some(qualname.clone());
qualname
}
fn make_qualname(&mut self, function_name: Option<&str>) -> String {
let stack_size = self.code_stack.len();
assert!(stack_size >= 1);
let current_obj_name = self.current_code_info().metadata.name.clone();
// If we're at the module level (stack_size == 1), qualname is just the name
if stack_size <= 1 {
return current_obj_name;
}
// Check parent scope
let mut parent_idx = stack_size - 2;
let mut parent = &self.code_stack[parent_idx];
let mut parent_scope = self
.symbol_table_stack
.get(parent_idx)
.map(|table| table.typ);
// CPython skips both generic-parameter scopes and annotation scopes
// when building qualnames for the contained function/class code object.
if matches!(
parent_scope,
Some(
CompilerScope::TypeParams
| CompilerScope::Annotation
| CompilerScope::TypeAlias
| CompilerScope::TypeVariable,
)
) || parent.metadata.name.starts_with("<generic parameters of ")
{
if stack_size == 2 {
// If we're immediately within the module, qualname is just the name
return current_obj_name;
}
// Use grandparent
parent_idx = stack_size - 3;
parent = &self.code_stack[parent_idx];
parent_scope = self
.symbol_table_stack
.get(parent_idx)
.map(|table| table.typ);
}
// Check if this is a global class/function.
// CPython compiler_set_qualname() only applies this GLOBAL_EXPLICIT
// shortcut to function, async-function, and class scopes. Annotation
// scopes, including type-alias value scopes, still inherit the parent
// function's .<locals> qualname.
let mut force_global = false;
let current_scope = self
.code_stack
.last()
.map(|code| self.symbol_table_stack[code.symbol_table_index].typ);
if matches!(
current_scope,
Some(CompilerScope::Function | CompilerScope::AsyncFunction | CompilerScope::Class)
) {
if stack_size > self.symbol_table_stack.len() {
// We might be in a situation where symbol table isn't pushed yet
// In this case, check the parent symbol table
if let Some(parent_table) = self.symbol_table_stack.last()
&& let Some(symbol) = parent_table.lookup(&current_obj_name.clone().into())
&& symbol.scope == SymbolScope::GlobalExplicit
{
force_global = true;
}
} else if let Some(_current_table) = self.symbol_table_stack.last() {
// Mangle the name if necessary (for private names in classes)
let target = &current_obj_name.clone().into();
let mangled_name = self.mangle(target);
// Look up in parent symbol table to check scope
if self.symbol_table_stack.len() >= 2 {
let parent_table = &self.symbol_table_stack[self.symbol_table_stack.len() - 2];
if let Some(symbol) = parent_table.lookup(&mangled_name)
&& symbol.scope == SymbolScope::GlobalExplicit
{
force_global = true;
}
}
}
}
// Build the prefix the current name is qualified by, if any
let base = if force_global {
// For global symbols, qualname is just the name
None
} else {
// Check parent scope type
let parent_obj_name = &parent.metadata.name;
// Determine if parent is a function-like scope
let is_function_parent = matches!(
parent_scope,
Some(
CompilerScope::Function | CompilerScope::AsyncFunction | CompilerScope::Lambda
)
);
// Use parent's qualname if available, otherwise use parent_obj_name
let parent_qualname = parent.metadata.qualname.as_ref().unwrap_or(parent_obj_name);
if is_function_parent {
// For functions, append .<locals> to parent qualname
Some(format!("{parent_qualname}.<locals>"))
} else if parent_qualname == "<module>" {
// Module level, nothing to qualify by
None
} else {
// For classes and other scopes, use parent's qualname directly
Some(parent_qualname.clone())
}
};
// An annotation scope is compiled in the scope enclosing the function it
// annotates, so the function itself is missing from the prefix above.
let base = match (base, function_name) {
(Some(base), Some(function_name)) => Some(format!("{base}.{function_name}")),
(None, Some(function_name)) => Some(function_name.to_owned()),
(base, None) => base,
};
match base {
Some(base) => format!("{base}.{current_obj_name}"),
None => current_obj_name,
}
}
fn compile_program(
&mut self,
body: &ast::ModModule,
symbol_table: SymbolTable,
) -> CompileResult<()> {
let size_before = self.code_stack.len();
// Set future_annotations from symbol table (detected during symbol table scan)
self.future_annotations = symbol_table.future_annotations;
let future_features = self.future_features;
self.current_code_info().flags |= future_features;
if self.future_annotations {
self.future_features
.insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
self.current_code_info()
.flags
.insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
}
if symbol_table.is_coroutine {
self.current_code_info()
.flags
.insert(bytecode::CodeFlags::COROUTINE);
}
// Module-level __conditional_annotations__ cell
let has_module_cond_ann = Self::scope_needs_conditional_annotations_cell(&symbol_table);
if has_module_cond_ann {
self.current_code_info()
.metadata
.cellvars
.insert("__conditional_annotations__".to_string());
}
self.symbol_table_stack.push(symbol_table);
self.emit_resume_for_scope(CompilerScope::Module, 1);
emit!(self, PseudoInstruction::AnnotationsPlaceholder);
let (doc, statements) = split_doc_with_range(&body.body, &self.opts);
let module_start_loc = self.module_start_location(&body.body);
let annotations_used = self.current_symbol_table().annotations_used;
// Handle annotation bookkeeping before the docstring assignment, as
// codegen_body() does after _PyCodegen_Module() inserts the prefix set.
if Self::scope_needs_conditional_annotations_cell(self.current_symbol_table()) {
self.set_source_range(module_start_loc);
emit!(self, Instruction::BuildSet { count: 0 });
self.store_name(&"__conditional_annotations__".into())?;
}
if self.future_annotations && annotations_used {
self.set_source_range(module_start_loc);
emit!(self, Instruction::SetupAnnotations);
}
if let Some((value, range)) = doc {
let saved_range = self.current_source_range;
self.set_source_range(range);
self.emit_load_const(ConstantData::Str {
value: value.into(),
});
let doc = self.name("__doc__");
emit!(self, Instruction::StoreName { namei: doc });
self.set_no_location();
self.set_source_range(saved_range);
}
// Compile all statements
self.compile_statements(statements)?;
if annotations_used && !self.future_annotations {
self.compile_module_annotation_setup_sequence(statements, module_start_loc)?;
}
assert_eq!(self.code_stack.len(), size_before);
// Match _PyCodegen_AddReturnAtEnd(): implicit scope epilogues start
// without a source location and receive one later via CFG line
// propagation.
self.emit_return_const_no_location(ConstantData::None);
Ok(())
}
fn compile_program_single(
&mut self,
body: &[ast::Stmt],
symbol_table: SymbolTable,
) -> CompileResult<()> {
self.interactive = true;
// Set future_annotations from symbol table (detected during symbol table scan)
self.future_annotations = symbol_table.future_annotations;
let future_features = self.future_features;
self.current_code_info().flags |= future_features;
if self.future_annotations {
self.future_features
.insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
self.current_code_info()
.flags
.insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
}
if symbol_table.is_coroutine {
self.current_code_info()
.flags
.insert(bytecode::CodeFlags::COROUTINE);
}
// Module-level __conditional_annotations__ cell
if Self::scope_needs_conditional_annotations_cell(&symbol_table) {
self.current_code_info()
.metadata
.cellvars
.insert("__conditional_annotations__".to_string());
}
self.symbol_table_stack.push(symbol_table);
let module_start_loc = self.module_start_location(body);
self.emit_resume_for_scope(CompilerScope::Module, 1);
emit!(self, PseudoInstruction::AnnotationsPlaceholder);
let annotations_used = self.current_symbol_table().annotations_used;
// Handle annotations based on future_annotations flag
if self.current_symbol_table().has_conditional_annotations {
self.set_source_range(module_start_loc);
emit!(self, Instruction::BuildSet { count: 0 });
self.store_name(&"__conditional_annotations__".into())?;
}
if self.future_annotations && annotations_used {
self.set_source_range(module_start_loc);
// PEP 563: Initialize __annotations__ dict
emit!(self, Instruction::SetupAnnotations);
}
self.compile_statements(body)?;
if annotations_used && !self.future_annotations {
self.compile_module_annotation_setup_sequence(body, module_start_loc)?;
}
self.emit_return_const_no_location(ConstantData::None);
Ok(())
}
fn compile_block_expr(
&mut self,
body: &[ast::Stmt],
symbol_table: SymbolTable,
) -> CompileResult<()> {
let future_features = self.future_features;
self.current_code_info().flags |= future_features;
if symbol_table.is_coroutine {
self.current_code_info()
.flags
.insert(bytecode::CodeFlags::COROUTINE);
}
self.symbol_table_stack.push(symbol_table);
self.emit_resume_for_scope(CompilerScope::Module, 1);
if let Some((last_statement, statements)) = body.split_last() {
self.compile_statements(statements)?;
match last_statement {
ast::Stmt::Expr(ast::StmtExpr { value, .. }) => {
self.compile_expression(value)?;
}
ast::Stmt::FunctionDef(ast::StmtFunctionDef {
name,
parameters,
body,
decorator_list,
returns,
type_params,
is_async,
..
}) => {
validate_duplicate_params(parameters).map_err(|e| self.error(e))?;
self.compile_function_def(
name.id(),
parameters,
body,
decorator_list,
returns.as_deref(),
*is_async,
type_params.as_deref(),
true,
)?;
}
ast::Stmt::ClassDef(ast::StmtClassDef {
name,
body,
decorator_list,
type_params,
arguments,
..
}) => {
self.compile_class_def(
name.id(),
body,
decorator_list,
type_params.as_deref(),
arguments.as_deref(),
true,
)?;
}
_ => {
self.compile_statement(last_statement)?;
self.emit_load_const(ConstantData::None);
}
}
} else {
self.emit_load_const(ConstantData::None);
}
self.emit_return_value();
Ok(())
}
// Compile statement in eval mode:
fn compile_eval(
&mut self,
expression: &ast::ModExpression,
symbol_table: SymbolTable,
) -> CompileResult<()> {
let future_features = self.future_features;
self.current_code_info().flags |= future_features;
if symbol_table.is_coroutine {
self.current_code_info()
.flags
.insert(bytecode::CodeFlags::COROUTINE);
}
self.symbol_table_stack.push(symbol_table);
self.emit_resume_for_scope(CompilerScope::Module, 1);
self.compile_expression(&expression.body)?;
self.emit_return_value();
// The return belongs to no expression, so the exit block can take its
// location from whichever path reaches it.
self.set_no_location();
Ok(())
}
fn compile_statements(&mut self, statements: &[ast::Stmt]) -> CompileResult<()> {
for statement in statements {
self.compile_statement(statement)?;
}
Ok(())
}
fn compile_with_body_statements(&mut self, statements: &[ast::Stmt]) -> CompileResult<()> {
for statement in statements {
self.compile_statement(statement)?;
}
Ok(())
}
/// CPython `codegen_call_exit_with_nones()`.
fn compile_call_exit_with_nones(&mut self) {
self.emit_load_const(ConstantData::None);
self.emit_load_const(ConstantData::None);
self.emit_load_const(ConstantData::None);
emit!(self, Instruction::Call { argc: 3 });
}
/// CPython `codegen_with_except_finish()`.
fn compile_with_except_finish(&mut self, cleanup_block: BlockIdx) {
let suppress_block = self.new_block();
emit!(self, Instruction::ToBool);
self.set_no_location();
emit!(
self,
Instruction::PopJumpIfTrue {
delta: suppress_block
}
);
self.set_no_location();
emit!(self, Instruction::Reraise { depth: 2 });
self.set_no_location();
self.use_cpython_label_block(suppress_block);
emit!(self, Instruction::PopTop);
self.set_no_location();
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
emit!(self, Instruction::PopExcept);
self.set_no_location();
emit!(self, Instruction::PopTop);
self.set_no_location();
emit!(self, Instruction::PopTop);
self.set_no_location();
emit!(self, Instruction::PopTop);
self.set_no_location();
let exit_block = self.new_block();
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: exit_block }
);
self.set_no_location();
self.use_cpython_label_block(cleanup_block);
emit!(self, Instruction::Copy { i: 3 });
self.set_no_location();
emit!(self, Instruction::PopExcept);
self.set_no_location();
emit!(self, Instruction::Reraise { depth: 1 });
self.set_no_location();
self.use_cpython_label_block(exit_block);
}
fn compile_loop_body_statements(&mut self, statements: &[ast::Stmt]) -> CompileResult<()> {
self.compile_statements(statements)
}
fn scope_needs_conditional_annotations_cell(symbol_table: &SymbolTable) -> bool {
match symbol_table.typ {
CompilerScope::Module | CompilerScope::Class => {
symbol_table.has_conditional_annotations
}
_ => false,
}
}
fn load_name(&mut self, name: &Name) -> CompileResult<()> {
self.compile_name(name, NameUsage::Load)
}
fn store_name(&mut self, name: &Name) -> CompileResult<()> {
self.compile_name(name, NameUsage::Store)
}
fn emit_no_location_exception_name_cleanup(&mut self, name: &Name) -> CompileResult<()> {
// CPython codegen_try_except() emits `name = None; del name`
// with NO_LOCATION for `except ... as name` cleanup.
self.emit_load_const(ConstantData::None);
self.set_no_location();
self.store_name(name)?;
self.set_no_location();
self.compile_name(name, NameUsage::Delete)?;
self.set_no_location();
Ok(())
}
fn mangle<'a>(&self, name: &'a Name) -> Cow<'a, Name> {
// Use private from current code unit for name mangling
let private = self
.code_stack
.last()
.and_then(|info| info.private.as_ref());
let mangled_names = self.current_symbol_table().mangled_names.as_ref();
symboltable::maybe_mangle_name(private.map(Name::from).as_ref(), mangled_names, name)
}
fn module_name_declared_global_in_nested_scope(table: &SymbolTable, name: &Name) -> bool {
table.sub_tables.iter().any(|subtable| {
(!subtable.comp_inlined
&& subtable
.lookup(name)
.is_some_and(|symbol| symbol.scope == SymbolScope::GlobalExplicit))
|| Self::module_name_declared_global_in_nested_scope(subtable, name)
})
}
// = compiler_nameop
fn compile_name(&mut self, name: &Name, usage: NameUsage) -> CompileResult<()> {
enum NameOp {
Fast,
Global,
Deref,
Name,
DictOrGlobals, // PEP 649: can_see_class_scope
}
let name = self.mangle(name);
// Special handling for __debug__
if NameUsage::Load == usage && name.as_str() == "__debug__" {
self.emit_load_const(ConstantData::Boolean {
value: self.opts.optimize == 0,
});
return Ok(());
}
// Determine the operation type based on symbol scope
let is_function_like = self.ctx.in_func();
// Look up the symbol, handling ast::TypeParams and Annotation scopes specially
let (symbol_scope, can_see_class_scope, class_declared_global) = {
let current_idx = self.symbol_table_stack.len() - 1;
let current_table = &self.symbol_table_stack[current_idx];
let is_typeparams = current_table.typ == CompilerScope::TypeParams;
let is_annotation = matches!(
current_table.typ,
CompilerScope::Annotation | CompilerScope::TypeAlias | CompilerScope::TypeVariable
);
let can_see_class = current_table.can_see_class_scope;
// First try to find in current table
let symbol = current_table.lookup(&name);
// If not found and we're in ast::TypeParams or Annotation scope, try parent scope
let symbol = if symbol.is_none() && (is_typeparams || is_annotation) {
self.symbol_table_stack
.get(self.symbol_table_stack.len() - 2) // Try to get parent index
.expect("Symbol has no parent! This is a compiler bug.")
.lookup(&name)
} else {
symbol
};
let class_declared_global = can_see_class
&& self.symbol_table_stack[..current_idx]
.iter()
.rev()
.find(|table| table.typ == CompilerScope::Class)
.and_then(|table| table.lookup(&name))
.is_some_and(|symbol| symbol.flags.contains(SymbolFlags::DEF_GLOBAL));
(
symbol.map(|s| s.scope),
can_see_class,
class_declared_global,
)
};
// Special handling for class scope implicit cell variables.
// __classdict__: only LOAD uses Cell (stores go to class namespace)
// __conditional_annotations__: both LOAD and STORE use Cell (it's a mutable set
// that the annotation scope accesses through the closure)
let symbol_scope = {
let current_table = self.current_symbol_table();
if current_table.typ == CompilerScope::Class
&& !self.current_code_info().in_inlined_comp
&& ((usage == NameUsage::Load
&& (matches!(
name.as_str(),
"__classdict__" | "__conditional_annotations__"
)))
|| (name.as_str() == "__conditional_annotations__"
&& usage == NameUsage::Store))
{
Some(SymbolScope::Cell)
} else {
symbol_scope
}
};
// In annotation or type params scope, missing symbols are treated as global implicit
// This allows referencing global names like Union, Optional, etc. that are imported
// at module level but not explicitly bound in the function scope
let actual_scope = match symbol_scope {
Some(scope) => scope,
None => {
let current_table = self.current_symbol_table();
if matches!(
current_table.typ,
CompilerScope::Annotation
| CompilerScope::TypeAlias
| CompilerScope::TypeVariable
| CompilerScope::TypeParams
) {
SymbolScope::GlobalImplicit
} else if matches!(
name.as_str(),
"__name__"
| "__module__"
| "__qualname__"
| "__firstlineno__"
| "__doc__"
| "__static_attributes__"
| "__annotate__"
| "__annotate_func__"
| "__classdictcell__"
| "__classcell__"
) {
SymbolScope::Unknown
} else {
return Err(self.error(CodegenErrorType::SyntaxError(format!(
"the symbol '{name}' must be present in the symbol table"
))));
}
}
};
let module_global_from_nested_scope = {
let current_table = self.current_symbol_table();
current_table.typ == CompilerScope::Module
&& Self::module_name_declared_global_in_nested_scope(current_table, name.as_ref())
};
// Determine operation type based on scope
let op_type = match actual_scope {
SymbolScope::Free => NameOp::Deref,
SymbolScope::Cell => NameOp::Deref,
SymbolScope::Local => {
if module_global_from_nested_scope {
NameOp::Global
} else if is_function_like
|| self
.current_code_info()
.metadata
.fast_hidden
.get(name.as_str())
.is_some_and(|&hidden| hidden)
{
NameOp::Fast
} else {
NameOp::Name
}
}
SymbolScope::GlobalImplicit => {
// PEP 649: In annotation scope with class visibility, use DictOrGlobals
// to check classdict first before globals
if class_declared_global {
NameOp::Global
} else if can_see_class_scope && usage == NameUsage::Load {
NameOp::DictOrGlobals
} else if is_function_like {
NameOp::Global
} else {
NameOp::Name
}
}
SymbolScope::GlobalExplicit => {
// A global declared in the owning class body must bypass the
// classdict, but an explicit global inherited from an outer
// function still participates in DictOrGlobals lookup.
if can_see_class_scope && !class_declared_global && usage == NameUsage::Load {
NameOp::DictOrGlobals
} else {
NameOp::Global
}
}
SymbolScope::Unknown => {
if module_global_from_nested_scope {
NameOp::Global
} else {
NameOp::Name
}
}
};
// Generate appropriate instructions based on operation type
match op_type {
NameOp::Deref => {
let i = match actual_scope {
SymbolScope::Free => self.get_free_var_index(&name),
SymbolScope::Cell => self.get_cell_var_index(&name),
_ => unreachable!("Invalid scope for Deref operation"),
};
match usage {
NameUsage::Load => {
// ClassBlock (not inlined comp): LOAD_LOCALS first, then LOAD_FROM_DICT_OR_DEREF
if self.ctx.in_class
&& !self.ctx.in_func()
&& !self.current_code_info().in_inlined_comp
{
emit!(self, Instruction::LoadLocals);
emit!(self, Instruction::LoadFromDictOrDeref { i });
// can_see_class_scope: LOAD_DEREF(__classdict__) first
} else if can_see_class_scope {
let classdict_idx = self.get_free_var_index("__classdict__");
emit!(self, Instruction::LoadDeref { i: classdict_idx });
emit!(self, Instruction::LoadFromDictOrDeref { i });
} else {
emit!(self, Instruction::LoadDeref { i });
}
}
NameUsage::Store => emit!(self, Instruction::StoreDeref { i }),
NameUsage::Delete => emit!(self, Instruction::DeleteDeref { i }),
};
}
NameOp::Fast => {
let var_num = self.get_local_var_index(&name);
match usage {
NameUsage::Load => emit!(self, Instruction::LoadFast { var_num }),
NameUsage::Store => emit!(self, Instruction::StoreFast { var_num }),
NameUsage::Delete => emit!(self, Instruction::DeleteFast { var_num }),
};
}
NameOp::Global => {
let namei = self.get_global_name_index(&name);
match usage {
NameUsage::Load => {
self.emit_load_global(namei, false);
return Ok(());
}
NameUsage::Store => emit!(self, Instruction::StoreGlobal { namei }),
NameUsage::Delete => emit!(self, Instruction::DeleteGlobal { namei }),
};
}
NameOp::Name => {
let namei = self.get_global_name_index(&name);
match usage {
NameUsage::Load => {
if self.current_symbol_table().typ == CompilerScope::Class
&& self.current_code_info().in_inlined_comp
{
self.emit_load_global(namei, false);
} else {
emit!(self, Instruction::LoadName { namei });
}
}
NameUsage::Store => emit!(self, Instruction::StoreName { namei }),
NameUsage::Delete => emit!(self, Instruction::DeleteName { namei }),
};
}
NameOp::DictOrGlobals => {
// PEP 649: First check classdict (from __classdict__ freevar), then globals
let idx = self.get_global_name_index(&name);
debug_assert!(usage == NameUsage::Load);
let classdict_idx = self.get_free_var_index("__classdict__");
emit!(self, Instruction::LoadDeref { i: classdict_idx });
emit!(self, Instruction::LoadFromDictOrGlobals { i: idx });
}
}
Ok(())
}
fn compile_statement(&mut self, statement: &ast::Stmt) -> CompileResult<()> {
trace!("Compiling {statement:?}");
let prev_source_range = self.current_source_range;
self.set_source_range(statement.range());
// `from __future__` still executes that `from` statement at runtime, so the
// ImportFrom is compiled down below as well.
if let ast::Stmt::ImportFrom(ast::StmtImportFrom {
module,
names,
level,
..
}) = &statement
&& *level == 0
&& module.as_ref().map(|id| id.as_str()) == Some("__future__")
{
self.compile_future_features(names)?;
}
match &statement {
ast::Stmt::Import(ast::StmtImport { names, .. }) => {
// import a, b, c as d
for name in names {
let name = &name;
self.emit_load_const(ConstantData::Integer {
value: num_traits::Zero::zero(),
});
self.emit_load_const(ConstantData::None);
let namei = self.name(&name.name);
emit!(self, Instruction::ImportName { namei });
if let Some(alias) = &name.asname {
let parts: Vec<&str> = name.name.split('.').skip(1).collect();
for (i, part) in parts.iter().enumerate() {
let namei = self.name(part);
emit!(self, Instruction::ImportFrom { namei });
if i < parts.len() - 1 {
emit!(self, Instruction::Swap { i: 2 });
emit!(self, Instruction::PopTop);
}
}
self.store_name(alias.id())?;
if !parts.is_empty() {
emit!(self, Instruction::PopTop);
}
} else {
self.store_name(&name.name.split('.').next().unwrap().into())?
}
}
}
ast::Stmt::ImportFrom(ast::StmtImportFrom {
level,
module,
names,
..
}) => {
let import_star = names.first().is_some_and(|n| &n.name == "*");
let from_list = names
.iter()
.map(|n| ConstantData::Str {
value: n.name.as_str().into(),
})
.collect();
// from .... import (*fromlist)
self.emit_load_const(ConstantData::Integer {
value: (*level).into(),
});
self.emit_load_const(ConstantData::Tuple {
elements: from_list,
});
let module_name = module.as_ref().map_or("", |s| s.as_str());
let module_idx = self.name(module_name);
emit!(self, Instruction::ImportName { namei: module_idx });
if import_star {
// from .... import *
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::ImportStar
}
);
emit!(self, Instruction::PopTop);
self.set_no_location();
} else {
// from mod import a, b as c
for name in names {
let name = &name;
let idx = self.name(name.name.as_str());
// import symbol from module:
emit!(self, Instruction::ImportFrom { namei: idx });
// Store module under proper name:
if let Some(alias) = &name.asname {
self.store_name(alias.id())?
} else {
self.store_name(name.name.id())?
}
}
// Pop module from stack:
emit!(self, Instruction::PopTop);
}
}
ast::Stmt::Expr(ast::StmtExpr { value, .. }) => {
// Optimize away constant expressions with no side effects.
// In interactive mode, always compile (to print the result).
let dominated_by_interactive =
self.interactive && !self.ctx.in_func() && !self.ctx.in_class;
if !dominated_by_interactive && value.is_constant() {
emit!(self, Instruction::Nop);
} else {
let statement_range = self.current_source_range;
self.compile_expression(value)?;
if dominated_by_interactive {
// The printing belongs to the statement, not to whatever
// the expression left behind.
self.set_source_range(statement_range);
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::Print
}
);
}
emit!(self, Instruction::PopTop);
self.set_no_location();
}
}
ast::Stmt::Global(_) | ast::Stmt::Nonlocal(_) => {
// Handled during symbol table construction.
}
ast::Stmt::If(ast::StmtIf {
test,
body,
elif_else_clauses,
..
}) => {
self.enter_conditional_block();
self.compile_if(test, body, elif_else_clauses, statement.range())?;
self.leave_conditional_block();
self.set_source_range(statement.range());
}
ast::Stmt::While(ast::StmtWhile {
test,
body,
orelse,
range,
..
}) => self.compile_while(test, body, orelse, *range)?,
ast::Stmt::With(ast::StmtWith {
items,
body,
is_async,
..
}) => self.compile_with(items, body, *is_async)?,
ast::Stmt::For(ast::StmtFor {
target,
iter,
body,
orelse,
is_async,
range,
..
}) => self.compile_for(target, iter, body, orelse, *is_async, *range)?,
ast::Stmt::Match(ast::StmtMatch { subject, cases, .. }) => {
self.compile_match(subject, cases)?
}
ast::Stmt::Raise(ast::StmtRaise {
exc, cause, range, ..
}) => {
let kind = match exc {
Some(value) => {
self.compile_expression(value)?;
match cause {
Some(cause) => {
self.compile_expression(cause)?;
bytecode::RaiseKind::RaiseCause
}
None => bytecode::RaiseKind::Raise,
}
}
None => bytecode::RaiseKind::BareRaise,
};
self.set_source_range(*range);
emit!(self, Instruction::RaiseVarargs { argc: kind });
}
ast::Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
is_star,
..
}) => {
self.enter_conditional_block();
if *is_star {
self.compile_try_star_statement(body, handlers, orelse, finalbody)?
} else {
self.compile_try_statement(body, handlers, orelse, finalbody)?
}
self.leave_conditional_block();
}
ast::Stmt::FunctionDef(ast::StmtFunctionDef {
name,
parameters,
body,
decorator_list,
returns,
type_params,
is_async,
..
}) => {
validate_duplicate_params(parameters).map_err(|e| self.error(e))?;
self.compile_function_def(
name.id(),
parameters,
body,
decorator_list,
returns.as_deref(),
*is_async,
type_params.as_deref(),
false,
)?
}
ast::Stmt::ClassDef(ast::StmtClassDef {
name,
body,
decorator_list,
type_params,
arguments,
..
}) => self.compile_class_def(
name.id(),
body,
decorator_list,
type_params.as_deref(),
arguments.as_deref(),
false,
)?,
ast::Stmt::Assert(assert_stmt) => {
let ast::StmtAssert {
test, msg, range, ..
} = assert_stmt;
self.check_assert(assert_stmt)?;
// if some flag, ignore all assert statements!
if self.opts.optimize == 0 {
let after_block = self.new_block();
self.compile_jump_if_inner(test, true, after_block, Some(*range))?;
self.set_source_range(*range);
emit!(
self,
Instruction::LoadCommonConstant {
idx: bytecode::CommonConstant::AssertionError
}
);
if let Some(e) = msg {
self.compile_expression(e)?;
self.set_source_range(*range);
emit!(self, Instruction::Call { argc: 0 });
}
self.set_source_range(test.range());
emit!(
self,
Instruction::RaiseVarargs {
argc: bytecode::RaiseKind::Raise,
}
);
self.use_cpython_label_block(after_block);
} else {
// Optimized-out asserts still need to consume any nested
// scope symbol tables they contain so later nested scopes
// stay aligned with AST traversal order.
self.consume_skipped_nested_scopes_in_expr(test)?;
if let Some(expr) = msg {
self.consume_skipped_nested_scopes_in_expr(expr)?;
}
}
}
ast::Stmt::Break(_) => {
// Unwind fblock stack until we find a loop, emitting cleanup for each fblock
self.compile_break_continue(statement.range(), true)?;
}
ast::Stmt::Continue(_) => {
// Unwind fblock stack until we find a loop, emitting cleanup for each fblock
self.compile_break_continue(statement.range(), false)?;
}
ast::Stmt::Return(ast::StmtReturn { value, .. }) => {
if !self.ctx.in_func() {
return Err(
self.error_ranged(CodegenErrorType::InvalidReturn, statement.range())
);
}
let prev_source_range = self.current_source_range;
let stmt_range = statement.range();
match value {
Some(v) => {
if self.ctx.func == FunctionContext::AsyncFunction
&& self.current_symbol_table().is_generator
{
return Err(self.error_ranged(
CodegenErrorType::AsyncReturnValue,
statement.range(),
));
}
let debug_constant = matches!(
&**v,
ast::Expr::Name(ast::ExprName { id, ctx, .. })
if matches!(ctx, ast::ExprContext::Load)
&& id.as_str() == "__debug__"
);
let folded_constant = if self.is_constant_expr(v) || debug_constant {
self.try_fold_constant_expr(v)?
} else {
None
};
let preserve_tos = folded_constant.is_none();
let mut return_range = stmt_range;
if preserve_tos {
self.compile_expression(v)?;
} else {
return_range = v.range();
self.set_source_range(v.range());
emit!(self, Instruction::Nop);
}
let source = self.source_file.to_source_code();
if source.line_index(v.range().start())
!= source.line_index(stmt_range.start())
{
return_range = stmt_range;
self.set_source_range(stmt_range);
emit!(self, Instruction::Nop);
}
self.set_source_range(return_range);
let unwind_loc = self.unwind_fblock_stack(preserve_tos, false)?;
if let Some(loc) = unwind_loc {
self.set_source_range(loc);
match folded_constant {
Some(constant) => self.emit_return_const(constant),
None => {
self.emit_return_value();
}
}
} else {
match folded_constant {
Some(constant) => self.emit_return_const_no_location(constant),
None => {
self.emit_return_value();
self.set_no_location();
}
}
}
}
None => {
self.set_source_range(stmt_range);
emit!(self, Instruction::Nop);
// Unwind fblock stack with preserve_tos=false (no value to preserve)
let unwind_loc = self.unwind_fblock_stack(false, false)?;
if let Some(loc) = unwind_loc {
self.set_source_range(loc);
self.emit_return_const(ConstantData::None);
} else {
self.emit_return_const_no_location(ConstantData::None);
}
}
}
self.set_source_range(prev_source_range);
}
ast::Stmt::Assign(ast::StmtAssign {
targets,
value,
range,
..
}) => {
self.compile_expression(value)?;
for (i, target) in targets.iter().enumerate() {
if i + 1 != targets.len() {
self.set_source_range(*range);
emit!(self, Instruction::Copy { i: 1 });
}
self.compile_store(target)?;
}
}
ast::Stmt::AugAssign(ast::StmtAugAssign {
target, op, value, ..
}) => self.compile_augassign(target, *op, value)?,
ast::Stmt::AnnAssign(ast::StmtAnnAssign {
target,
annotation,
value,
simple,
range,
..
}) => {
self.compile_annotated_assign(
target,
annotation,
value.as_deref(),
*simple,
*range,
)?;
// Bare annotations in function scope emit no code; restore
// source range so subsequent instructions keep the correct line.
if value.is_none() && self.ctx.in_func() {
self.set_source_range(prev_source_range);
}
}
ast::Stmt::Delete(ast::StmtDelete { targets, .. }) => {
for target in targets {
self.compile_delete(target)?;
}
}
ast::Stmt::Pass(_) => {
emit!(self, Instruction::Nop); // NOP for line tracing
}
ast::Stmt::TypeAlias(ast::StmtTypeAlias {
name,
type_params,
value,
range,
..
}) => {
let Some(name) = name.as_name_expr() else {
return Err(self.error(CodegenErrorType::SyntaxError(
"type alias expect name".to_owned(),
)));
};
let name_string = name.id();
if let Some(type_params) = type_params {
self.set_source_range(*range);
self.push_symbol_table()?;
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get().to_u32();
let scope_name = format!("<generic parameters of {name_string}>");
self.enter_scope(&scope_name, CompilerScope::TypeParams, key, lineno)?;
// TypeParams scope is function-like
let prev_ctx = self.ctx;
self.ctx = CompileContext {
in_class: prev_ctx.in_class,
func: FunctionContext::Function,
in_async_scope: false,
};
self.set_source_range(*range);
self.emit_load_const(ConstantData::Str {
value: name_string.as_str().into(),
});
self.compile_type_params(type_params)?;
self.compile_typealias_value_closure(name_string, value, *range)?;
self.set_source_range(*range);
emit!(self, Instruction::BuildTuple { count: 3 });
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::TypeAlias
}
);
emit!(self, Instruction::ReturnValue);
let code = self.exit_scope();
self.ctx = prev_ctx;
self.set_source_range(*range);
self.make_closure(code, bytecode::MakeFunctionFlags::new())?;
self.set_source_range(*range);
emit!(self, Instruction::PushNull);
emit!(self, Instruction::Call { argc: 0 });
} else {
self.set_source_range(*range);
self.emit_load_const(ConstantData::Str {
value: name_string.as_str().into(),
});
self.emit_load_const(ConstantData::None);
self.compile_typealias_value_closure(name_string, value, *range)?;
self.set_source_range(*range);
emit!(self, Instruction::BuildTuple { count: 3 });
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::TypeAlias
}
);
}
self.set_source_range(*range);
self.store_name(name_string)?;
}
ast::Stmt::IpyEscapeCommand(stmt) => {
return Err(self.error_ranged(
CodegenErrorType::SyntaxError("invalid syntax".to_owned()),
stmt.range,
));
}
}
Ok(())
}
fn compile_delete(&mut self, expression: &ast::Expr) -> CompileResult<()> {
let prev_source_range = self.current_source_range;
self.set_source_range(expression.range());
let result = (|| -> CompileResult<()> {
match &expression {
ast::Expr::Name(ast::ExprName { id, .. }) => {
self.compile_name(id, NameUsage::Delete)?
}
ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
self.compile_expression(value)?;
let namei = self.name(attr.as_str());
self.set_source_range(self.update_start_location_to_match_attr(
expression.range(),
expression.range(),
attr.as_str(),
));
emit!(self, Instruction::DeleteAttr { namei });
}
ast::Expr::Subscript(ast::ExprSubscript {
value, slice, ctx, ..
}) => {
self.compile_subscript(value, slice, *ctx)?;
}
ast::Expr::Tuple(ast::ExprTuple { elts, .. })
| ast::Expr::List(ast::ExprList { elts, .. }) => {
for element in elts {
self.compile_delete(element)?;
}
}
ast::Expr::BinOp(_) | ast::Expr::UnaryOp(_) => {
return Err(self.error(CodegenErrorType::Delete("expression")));
}
_ => return Err(self.error(CodegenErrorType::Delete(expression.python_name()))),
}
Ok(())
})();
self.set_source_range(prev_source_range);
result
}
fn enter_function(&mut self, name: &str, parameters: &ast::Parameters) -> CompileResult<()> {
self.push_output(
bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED,
parameters.posonlyargs.len().to_u32(),
parameters.args.len().to_u32(),
parameters.kwonlyargs.len().to_u32(),
name,
)?;
let args_iter = core::iter::empty()
.chain(&parameters.posonlyargs)
.chain(&parameters.args)
.map(|arg| &arg.parameter)
.chain(parameters.kwonlyargs.iter().map(|arg| &arg.parameter));
for name in args_iter {
self.varname(name.name.as_str());
}
if let Some(name) = parameters.vararg.as_deref() {
self.current_code_info().flags |= bytecode::CodeFlags::VARARGS;
self.varname(name.name.as_str());
}
if let Some(name) = parameters.kwarg.as_deref() {
self.current_code_info().flags |= bytecode::CodeFlags::VARKEYWORDS;
self.varname(name.name.as_str());
}
Ok(())
}
/// Push decorators onto the stack in source order.
/// For @dec1 @dec2 def foo(): stack becomes [dec1, dec2]
fn prepare_decorators(&mut self, decorator_list: &[ast::Decorator]) -> CompileResult<()> {
for decorator in decorator_list {
self.compile_expression(&decorator.expression)?;
}
Ok(())
}
/// Apply decorators: each decorator calls the function below it.
/// Stack: [dec1, dec2, func] → CALL 0 → [dec1, dec2(func)] → CALL 0 → [dec1(dec2(func))]
fn apply_decorators(&mut self, decorator_list: &[ast::Decorator]) {
for decorator in decorator_list.iter().rev() {
self.set_source_range(decorator.expression.range());
emit!(self, Instruction::Call { argc: 0 });
}
}
/// Compile type parameter bound or default in a separate scope and return closure
fn compile_type_param_bound_or_default(
&mut self,
expr: &ast::Expr,
name: &str,
allow_starred: bool,
) -> CompileResult<()> {
let expr_range = expr.range();
self.set_source_range(expr_range);
self.emit_load_const(ConstantData::Tuple {
elements: vec![ConstantData::Integer { value: 1.into() }],
});
// Push the next symbol table onto the stack
self.push_symbol_table()?;
// Get the current symbol table
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get().to_u32();
// Enter scope with the type parameter name
self.enter_scope(name, CompilerScope::TypeVariable, key, lineno)?;
self.configure_annotation_format_parameter();
self.emit_format_validation();
// TypeParams scope is function-like
let prev_ctx = self.ctx;
self.ctx = CompileContext {
in_class: prev_ctx.in_class,
func: FunctionContext::Function,
in_async_scope: false,
};
// Compile the expression
if allow_starred && matches!(expr, ast::Expr::Starred(_)) {
if let ast::Expr::Starred(starred) = expr {
self.compile_expression(&starred.value)?;
self.set_source_range(expr_range);
emit!(self, Instruction::UnpackSequence { count: 1 });
}
} else {
self.compile_expression(expr)?;
}
// Return value
self.set_source_range(expr_range);
emit!(self, Instruction::ReturnValue);
self.emit_return_const_no_location(ConstantData::None);
// Exit scope and create closure
let code = self.exit_scope();
self.ctx = prev_ctx;
self.set_source_range(expr_range);
self.make_closure(
code,
bytecode::MakeFunctionFlags::from([bytecode::MakeFunctionFlag::Defaults]),
)?;
Ok(())
}
fn compile_typealias_value_closure(
&mut self,
alias_name: &str,
value: &ast::Expr,
alias_range: TextRange,
) -> CompileResult<()> {
self.set_source_range(alias_range);
self.emit_load_const(ConstantData::Tuple {
elements: vec![ConstantData::Integer { value: 1.into() }],
});
self.push_symbol_table()?;
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get().to_u32();
self.enter_scope(alias_name, CompilerScope::TypeAlias, key, lineno)?;
self.configure_annotation_format_parameter();
self.emit_format_validation();
let prev_ctx = self.ctx;
self.ctx = CompileContext {
in_class: prev_ctx.in_class,
func: FunctionContext::Function,
in_async_scope: false,
};
self.compile_expression(value)?;
self.set_source_range(alias_range);
emit!(self, Instruction::ReturnValue);
let code = self.exit_scope();
self.ctx = prev_ctx;
self.set_source_range(alias_range);
self.make_closure(
code,
bytecode::MakeFunctionFlags::from([bytecode::MakeFunctionFlag::Defaults]),
)?;
Ok(())
}
/// Store each type parameter so it is accessible to the current scope, and leave a tuple of
/// all the type parameters on the stack. Handles default values per PEP 695.
fn compile_type_params(&mut self, type_params: &ast::TypeParams) -> CompileResult<()> {
let mut seen_default = false;
// First, compile each type parameter and store it
for type_param in &type_params.type_params {
match type_param {
ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
name,
bound,
default,
range,
..
}) => {
self.set_source_range(*range);
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});
if let Some(expr) = &bound {
self.compile_type_param_bound_or_default(expr, name.as_str(), false)?;
self.set_source_range(*range);
let intrinsic = if expr.is_tuple_expr() {
bytecode::IntrinsicFunction2::TypeVarWithConstraint
} else {
bytecode::IntrinsicFunction2::TypeVarWithBound
};
emit!(self, Instruction::CallIntrinsic2 { func: intrinsic });
} else {
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::TypeVar
}
);
}
if let Some(default_expr) = default {
seen_default = true;
self.compile_type_param_bound_or_default(
default_expr,
name.as_str(),
false,
)?;
self.set_source_range(*range);
emit!(
self,
Instruction::CallIntrinsic2 {
func: bytecode::IntrinsicFunction2::SetTypeparamDefault
}
);
} else if seen_default {
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(format!(
"non-default type parameter '{name}' follows default type parameter"
)),
*range,
));
}
self.set_source_range(*range);
emit!(self, Instruction::Copy { i: 1 });
self.store_name(name.id())?;
}
ast::TypeParam::ParamSpec(ast::TypeParamParamSpec {
name,
default,
range,
..
}) => {
self.set_source_range(*range);
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::ParamSpec
}
);
if let Some(default_expr) = default {
seen_default = true;
self.compile_type_param_bound_or_default(
default_expr,
name.as_str(),
false,
)?;
self.set_source_range(*range);
emit!(
self,
Instruction::CallIntrinsic2 {
func: bytecode::IntrinsicFunction2::SetTypeparamDefault
}
);
} else if seen_default {
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(format!(
"non-default type parameter '{name}' follows default type parameter"
)),
*range,
));
}
self.set_source_range(*range);
emit!(self, Instruction::Copy { i: 1 });
self.store_name(name.id())?;
}
ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple {
name,
default,
range,
..
}) => {
self.set_source_range(*range);
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::TypeVarTuple
}
);
if let Some(default_expr) = default {
// TypeVarTuple allows starred expressions
self.compile_type_param_bound_or_default(
default_expr,
name.as_str(),
true,
)?;
self.set_source_range(*range);
emit!(
self,
Instruction::CallIntrinsic2 {
func: bytecode::IntrinsicFunction2::SetTypeparamDefault
}
);
seen_default = true;
} else if seen_default {
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(format!(
"non-default type parameter '{name}' follows default type parameter"
)),
*range,
));
}
self.set_source_range(*range);
emit!(self, Instruction::Copy { i: 1 });
self.store_name(name.id())?;
}
};
}
if let Some(first) = type_params.type_params.first() {
self.set_source_range(first.range());
}
emit!(
self,
Instruction::BuildTuple {
count: u32::try_from(type_params.len()).unwrap(),
}
);
Ok(())
}
fn compile_try_statement(
&mut self,
body: &[ast::Stmt],
handlers: &[ast::ExceptHandler],
orelse: &[ast::Stmt],
finalbody: &[ast::Stmt],
) -> CompileResult<()> {
if finalbody.is_empty() {
return self.compile_try_except_no_finally(body, handlers, orelse);
}
let body_block = self.new_block();
let finally_except_block = self.new_block();
let exit_block = self.new_block();
let finally_cleanup_block = self.new_block();
emit!(
self,
PseudoInstruction::SetupFinally {
delta: finally_except_block
}
);
self.use_cpython_label_block(body_block);
let body_label = self.instr_sequence_label_for_block(body_block);
let finally_except_label = self.instr_sequence_label_for_block(finally_except_block);
self.push_fblock_labels(
FBlockType::FinallyTry,
body_label,
finally_except_label,
FBlockDatum::FinallyBody(finalbody.to_vec()),
)?;
if handlers.is_empty() {
self.compile_statements(body)?;
} else {
self.compile_try_except_no_finally(body, handlers, orelse)?;
}
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
self.pop_fblock_label(FBlockType::FinallyTry, body_label);
let symbol_table_cursors = self.current_symbol_table_cursors();
self.compile_statements(finalbody)?;
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: exit_block }
);
self.set_no_location();
self.set_symbol_table_cursors(symbol_table_cursors);
self.use_cpython_label_block(finally_except_block);
emit!(
self,
PseudoInstruction::SetupCleanup {
delta: finally_cleanup_block
}
);
self.set_no_location();
emit!(self, Instruction::PushExcInfo);
self.set_no_location();
self.push_fblock_labels(
FBlockType::FinallyEnd,
finally_except_label,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)?;
self.compile_statements(finalbody)?;
self.pop_fblock_label(FBlockType::FinallyEnd, finally_except_label);
emit!(self, Instruction::Reraise { depth: 0 });
self.set_no_location();
self.use_cpython_label_block(finally_cleanup_block);
emit!(self, Instruction::Copy { i: 3 });
self.set_no_location();
emit!(self, Instruction::PopExcept);
self.set_no_location();
emit!(self, Instruction::Reraise { depth: 1 });
self.set_no_location();
self.use_cpython_label_block(exit_block);
Ok(())
}
fn compile_try_except_no_finally(
&mut self,
body: &[ast::Stmt],
handlers: &[ast::ExceptHandler],
orelse: &[ast::Stmt],
) -> CompileResult<()> {
let body_block = self.new_block();
let handler_block = self.new_block();
let end_block = self.new_block();
let cleanup_block = self.new_block();
emit!(
self,
PseudoInstruction::SetupFinally {
delta: handler_block
}
);
self.use_cpython_label_block(body_block);
let body_label = self.instr_sequence_label_for_block(body_block);
self.push_fblock_labels(
FBlockType::TryExcept,
body_label,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)?;
self.compile_statements(body)?;
self.pop_fblock_label(FBlockType::TryExcept, body_label);
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
// The symtable stores child scopes in AST visit order
// (body, handlers, orelse), while codegen_try_except() emits orelse
// before the exception handlers. Keep the symbol table in symtable order
// and only move the codegen cursor while compiling orelse.
let handler_symbol_table_cursors = self.current_symbol_table_cursors();
self.consume_skipped_nested_scopes_in_except_handlers(handlers)?;
self.compile_statements(orelse)?;
let after_orelse_symbol_table_cursors = self.current_symbol_table_cursors();
self.set_symbol_table_cursors(handler_symbol_table_cursors);
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: end_block }
);
self.set_no_location();
self.use_cpython_label_block(handler_block);
emit!(
self,
PseudoInstruction::SetupCleanup {
delta: cleanup_block
}
);
self.set_no_location();
emit!(self, Instruction::PushExcInfo);
self.set_no_location();
self.push_fblock_labels(
FBlockType::ExceptionHandler,
ir::InstructionSequenceLabel::NO_LABEL,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)?;
for (i, handler) in handlers.iter().enumerate() {
let ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
type_,
name,
body,
range: handler_range,
..
}) = handler;
self.set_source_range(*handler_range);
if type_.is_none() && i < handlers.len() - 1 {
return Err(self.error(CodegenErrorType::SyntaxError(
"default 'except:' must be last".to_owned(),
)));
}
let next_handler = self.new_block();
if let Some(exc_type) = type_ {
self.compile_expression(exc_type)?;
self.set_source_range(*handler_range);
emit!(self, Instruction::CheckExcMatch);
emit!(
self,
Instruction::PopJumpIfFalse {
delta: next_handler
}
);
}
if let Some(alias) = name {
let cleanup_end = self.new_block();
let cleanup_body = self.new_block();
self.store_name(alias.id())?;
emit!(self, PseudoInstruction::SetupCleanup { delta: cleanup_end });
self.use_cpython_label_block(cleanup_body);
let cleanup_body_label = self.instr_sequence_label_for_block(cleanup_body);
self.push_fblock_labels(
FBlockType::HandlerCleanup,
cleanup_body_label,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::ExceptionName(alias.as_str().to_owned()),
)?;
self.compile_statements(body)?;
self.pop_fblock_label(FBlockType::HandlerCleanup, cleanup_body_label);
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
emit!(self, Instruction::PopExcept);
self.set_no_location();
self.emit_no_location_exception_name_cleanup(alias.id())?;
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: end_block }
);
self.set_no_location();
self.use_cpython_label_block(cleanup_end);
self.emit_no_location_exception_name_cleanup(alias.id())?;
emit!(self, Instruction::Reraise { depth: 1 });
self.set_no_location();
} else {
let cleanup_body = self.new_block();
emit!(self, Instruction::PopTop);
self.use_cpython_label_block(cleanup_body);
let cleanup_body_label = self.instr_sequence_label_for_block(cleanup_body);
self.push_fblock_labels(
FBlockType::HandlerCleanup,
cleanup_body_label,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)?;
self.compile_statements(body)?;
self.pop_fblock_label(FBlockType::HandlerCleanup, cleanup_body_label);
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
emit!(self, Instruction::PopExcept);
self.set_no_location();
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: end_block }
);
self.set_no_location();
}
self.use_cpython_label_block(next_handler);
}
self.set_symbol_table_cursors(after_orelse_symbol_table_cursors);
self.pop_fblock_label(
FBlockType::ExceptionHandler,
ir::InstructionSequenceLabel::NO_LABEL,
);
emit!(self, Instruction::Reraise { depth: 0 });
self.set_no_location();
self.use_cpython_label_block(cleanup_block);
emit!(self, Instruction::Copy { i: 3 });
self.set_no_location();
emit!(self, Instruction::PopExcept);
self.set_no_location();
emit!(self, Instruction::Reraise { depth: 1 });
self.set_no_location();
self.use_cpython_label_block(end_block);
Ok(())
}
fn compile_try_star_statement(
&mut self,
body: &[ast::Stmt],
handlers: &[ast::ExceptHandler],
orelse: &[ast::Stmt],
finalbody: &[ast::Stmt],
) -> CompileResult<()> {
if finalbody.is_empty() {
return self.compile_try_star_except(body, handlers, orelse);
}
let body_block = self.new_block();
let finally_except_block = self.new_block();
let exit_block = self.new_block();
let finally_cleanup_block = self.new_block();
emit!(
self,
PseudoInstruction::SetupFinally {
delta: finally_except_block
}
);
self.use_cpython_label_block(body_block);
let body_label = self.instr_sequence_label_for_block(body_block);
let finally_except_label = self.instr_sequence_label_for_block(finally_except_block);
self.push_fblock_labels(
FBlockType::FinallyTry,
body_label,
finally_except_label,
FBlockDatum::FinallyBody(finalbody.to_vec()),
)?;
if handlers.is_empty() {
self.compile_statements(body)?;
} else {
self.compile_try_star_except(body, handlers, orelse)?;
}
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
self.pop_fblock_label(FBlockType::FinallyTry, body_label);
let symbol_table_cursors = self.current_symbol_table_cursors();
self.compile_statements(finalbody)?;
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: exit_block }
);
self.set_no_location();
self.set_symbol_table_cursors(symbol_table_cursors);
self.use_cpython_label_block(finally_except_block);
emit!(
self,
PseudoInstruction::SetupCleanup {
delta: finally_cleanup_block
}
);
self.set_no_location();
emit!(self, Instruction::PushExcInfo);
self.set_no_location();
self.push_fblock_labels(
FBlockType::FinallyEnd,
finally_except_label,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)?;
self.compile_statements(finalbody)?;
self.pop_fblock_label(FBlockType::FinallyEnd, finally_except_label);
emit!(self, Instruction::Reraise { depth: 0 });
self.set_no_location();
self.use_cpython_label_block(finally_cleanup_block);
emit!(self, Instruction::Copy { i: 3 });
self.set_no_location();
emit!(self, Instruction::PopExcept);
self.set_no_location();
emit!(self, Instruction::Reraise { depth: 1 });
self.set_no_location();
self.use_cpython_label_block(exit_block);
Ok(())
}
fn compile_try_star_except(
&mut self,
body: &[ast::Stmt],
handlers: &[ast::ExceptHandler],
orelse: &[ast::Stmt],
) -> CompileResult<()> {
// compiler_try_star_except
// Stack layout during handler processing: [prev_exc, orig, list, rest]
let body_block = self.new_block();
let handler_block = self.new_block();
let else_block = self.new_block();
let end_block = self.new_block();
let cleanup_block = self.new_block();
let reraise_star_block = self.new_block();
// SETUP_FINALLY for try body
emit!(
self,
PseudoInstruction::SetupFinally {
delta: handler_block
}
);
self.use_cpython_label_block(body_block);
let body_label = self.instr_sequence_label_for_block(body_block);
self.push_fblock_labels(
FBlockType::TryExcept,
body_label,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)?;
self.compile_statements(body)?;
self.pop_fblock_label(FBlockType::TryExcept, body_label);
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: else_block }
);
self.set_no_location();
// Exception handler entry
self.use_cpython_label_block(handler_block);
// Stack: [exc] (from exception table)
emit!(
self,
PseudoInstruction::SetupCleanup {
delta: cleanup_block
}
);
self.set_no_location();
// PUSH_EXC_INFO
emit!(self, Instruction::PushExcInfo);
self.set_no_location();
// Stack: [prev_exc, exc]
// Push EXCEPTION_GROUP_HANDLER fblock
self.push_fblock_labels(
FBlockType::ExceptionGroupHandler,
ir::InstructionSequenceLabel::NO_LABEL,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)?;
let n = handlers.len();
for (i, handler) in handlers.iter().enumerate() {
let ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
type_,
name,
body,
range: handler_range,
..
}) = handler;
let is_last_handler = i == n - 1;
self.set_source_range(*handler_range);
let next_handler_block = self.new_block();
let except_with_error_block = self.new_block();
let no_match_block = self.new_block();
if i == 0 {
// CPython initializes the except* work stack inside the first
// handler iteration in codegen_try_star_except().
emit!(self, Instruction::BuildList { count: 0 });
emit!(self, Instruction::Copy { i: 2 });
}
// Compile exception type. The public-AST validator allows a
// NULL type here, so codegen only emits CHECK_EG_MATCH when present.
if let Some(exc_type) = type_ {
self.compile_expression(exc_type)?;
self.set_source_range(*handler_range);
}
if type_.is_some() {
// Stack: [prev_exc, orig, list, rest, type]
// ADDOP(c, loc, CHECK_EG_MATCH);
emit!(self, Instruction::CheckEgMatch);
// Stack: [prev_exc, orig, list, new_rest, match]
// ADDOP_I(c, loc, COPY, 1);
// ADDOP_JUMP(c, loc, POP_JUMP_IF_NONE, no_match);
emit!(self, Instruction::Copy { i: 1 });
emit!(
self,
Instruction::PopJumpIfNone {
delta: no_match_block
}
);
}
// Handler matched
// Stack: [prev_exc, orig, list, new_rest, match]
// Note: CheckEgMatch already sets the matched exception as current exception
let cleanup_end_block = self.new_block();
let cleanup_body_block = self.new_block();
// Store match to name or pop
if let Some(alias) = name {
self.store_name(alias.id())?;
} else {
emit!(self, Instruction::PopTop); // pop match
}
// Stack: [prev_exc, orig, list, new_rest]
// HANDLER_CLEANUP fblock for handler body
emit!(
self,
PseudoInstruction::SetupCleanup {
delta: cleanup_end_block
}
);
self.use_cpython_label_block(cleanup_body_block);
let cleanup_body_label = self.instr_sequence_label_for_block(cleanup_body_block);
self.push_fblock_labels(
FBlockType::HandlerCleanup,
cleanup_body_label,
ir::InstructionSequenceLabel::NO_LABEL,
if let Some(alias) = name {
FBlockDatum::ExceptionName(alias.as_str().to_owned())
} else {
FBlockDatum::None
},
)?;
// Execute handler body
self.compile_statements(body)?;
// Handler body completed normally
self.pop_fblock_label(FBlockType::HandlerCleanup, cleanup_body_label);
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
// Cleanup name binding
if let Some(alias) = name {
self.emit_no_location_exception_name_cleanup(alias.id())?;
}
emit!(
self,
PseudoInstruction::JumpNoInterrupt {
delta: next_handler_block
}
);
self.set_no_location();
// Handler raised an exception (cleanup_end label)
self.use_cpython_label_block(cleanup_end_block);
// Stack: [prev_exc, orig, list, new_rest, lasti, raised_exc]
// (lasti is pushed because push_lasti=true in HANDLER_CLEANUP fblock)
// Cleanup name binding
if let Some(alias) = name {
self.emit_no_location_exception_name_cleanup(alias.id())?;
}
// LIST_APPEND(3) - append raised_exc to list
// Stack: [prev_exc, orig, list, new_rest, lasti, raised_exc]
// After pop: [prev_exc, orig, list, new_rest, lasti] (len=5)
// nth_value(i) = stack[len - i - 1], we need stack[2] = list
// stack[5 - i - 1] = 2 -> i = 2
emit!(self, Instruction::ListAppend { i: 3 });
self.set_no_location();
// Stack: [prev_exc, orig, list, new_rest, lasti]
// POP_TOP - pop lasti
emit!(self, Instruction::PopTop);
self.set_no_location();
// Stack: [prev_exc, orig, list, new_rest]
emit!(
self,
PseudoInstruction::JumpNoInterrupt {
delta: except_with_error_block
}
);
self.set_no_location();
self.use_cpython_label_block(next_handler_block);
emit!(self, Instruction::Nop);
self.set_no_location();
emit!(
self,
PseudoInstruction::JumpNoInterrupt {
delta: except_with_error_block
}
);
self.set_no_location();
self.use_cpython_label_block(no_match_block);
self.set_source_range(*handler_range);
emit!(self, Instruction::PopTop); // pop match (None)
// Stack: [prev_exc, orig, list, new_rest]
self.use_cpython_label_block(except_with_error_block);
if is_last_handler {
emit!(self, Instruction::ListAppend { i: 1 });
self.set_no_location();
emit!(
self,
PseudoInstruction::JumpNoInterrupt {
delta: reraise_star_block
}
);
self.set_no_location();
}
}
// Pop EXCEPTION_GROUP_HANDLER fblock
self.pop_fblock_label(
FBlockType::ExceptionGroupHandler,
ir::InstructionSequenceLabel::NO_LABEL,
);
let reraise_block = self.new_block();
// Reraise star block
self.use_cpython_label_block(reraise_star_block);
// Stack: [prev_exc, orig, list]
// CALL_INTRINSIC_2 PREP_RERAISE_STAR
// Takes 2 args (orig, list) and produces result
emit!(
self,
Instruction::CallIntrinsic2 {
func: bytecode::IntrinsicFunction2::PrepReraiseStar
}
);
self.set_no_location();
// Stack: [prev_exc, result]
// COPY 1
emit!(self, Instruction::Copy { i: 1 });
self.set_no_location();
// Stack: [prev_exc, result, result]
// POP_JUMP_IF_NOT_NONE reraise
emit!(
self,
Instruction::PopJumpIfNotNone {
delta: reraise_block
}
);
self.set_no_location();
// Stack: [prev_exc, result]
// Nothing to reraise
// POP_TOP - pop result (None)
emit!(self, Instruction::PopTop);
self.set_no_location();
// Stack: [prev_exc]
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
// POP_EXCEPT - restore previous exception context
emit!(self, Instruction::PopExcept);
self.set_no_location();
// Stack: []
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: end_block }
);
self.set_no_location();
// Reraise the result
self.use_cpython_label_block(reraise_block);
// Stack: [prev_exc, result]
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
emit!(self, Instruction::Swap { i: 2 });
self.set_no_location();
// Stack: [result, prev_exc]
// POP_EXCEPT
emit!(self, Instruction::PopExcept);
self.set_no_location();
// Stack: [result]
// RERAISE 0
emit!(self, Instruction::Reraise { depth: 0 });
self.set_no_location();
self.use_cpython_label_block(cleanup_block);
emit!(self, Instruction::Copy { i: 3 });
self.set_no_location();
emit!(self, Instruction::PopExcept);
self.set_no_location();
emit!(self, Instruction::Reraise { depth: 1 });
self.set_no_location();
self.use_cpython_label_block(else_block);
self.compile_statements(orelse)?;
self.use_cpython_label_block(end_block);
Ok(())
}
/// Compile default arguments
// = compiler_default_arguments
fn compile_default_arguments(
&mut self,
parameters: &ast::Parameters,
loc: TextRange,
) -> CompileResult<bytecode::MakeFunctionFlags> {
let mut funcflags = bytecode::MakeFunctionFlags::new();
// Handle positional defaults
let defaults: Vec<_> = core::iter::empty()
.chain(&parameters.posonlyargs)
.chain(&parameters.args)
.filter_map(|x| x.default.as_deref())
.collect();
if !defaults.is_empty() {
// Compile defaults and build tuple
for default in &defaults {
self.compile_expression(default)?;
}
self.set_source_range(loc);
emit!(
self,
Instruction::BuildTuple {
count: defaults.len().to_u32()
}
);
funcflags.insert(bytecode::MakeFunctionFlag::Defaults);
}
// Handle keyword-only defaults
let mut kw_with_defaults = vec![];
for kwonlyarg in &parameters.kwonlyargs {
if let Some(default) = &kwonlyarg.default {
kw_with_defaults.push((&kwonlyarg.parameter, default));
}
}
if !kw_with_defaults.is_empty() {
// Compile kwdefaults and build dict
for (arg, default) in &kw_with_defaults {
self.set_source_range(loc);
self.emit_load_const(ConstantData::Str {
value: self.mangle(arg.name().id()).as_str().into(),
});
self.compile_expression(default)?;
}
self.set_source_range(loc);
emit!(
self,
Instruction::BuildMap {
count: kw_with_defaults.len().to_u32(),
}
);
funcflags.insert(bytecode::MakeFunctionFlag::KwOnlyDefaults);
}
Ok(funcflags)
}
/// Compile function body and create function object
// = compiler_function_body
fn compile_function_body(
&mut self,
name: &str,
parameters: &ast::Parameters,
body: &[ast::Stmt],
is_async: bool,
funcflags: bytecode::MakeFunctionFlags,
closure_range: TextRange,
) -> CompileResult<()> {
// Always enter function scope
self.enter_function(name, parameters)?;
self.current_code_info()
.flags
.set(bytecode::CodeFlags::COROUTINE, is_async);
// Set up context
let prev_ctx = self.ctx;
self.ctx = CompileContext {
in_class: prev_ctx.in_class,
func: if is_async {
FunctionContext::AsyncFunction
} else {
FunctionContext::Function
},
// A function starts a new async scope only if it's async
in_async_scope: is_async,
};
// Set qualname
self.set_qualname();
// Handle docstring - store in co_consts[0] if present
let (doc_info, body) = split_doc_with_range(body, &self.opts);
let doc_str = doc_info.as_ref().map(|(doc, _)| doc);
if let Some(doc) = &doc_str {
// Docstring present: store in co_consts[0] and set HAS_DOCSTRING flag
self.current_code_info()
.metadata
.consts
.insert_full(ConstantData::Str {
value: (*doc).to_string().into(),
});
self.current_code_info().flags |= bytecode::CodeFlags::HAS_DOCSTRING;
}
let start_label = self.use_cpython_function_start_label();
// PEP 479: Wrap generator/coroutine body with StopIteration handler
let is_gen = is_async || self.current_symbol_table().is_generator;
let stop_iteration_block = if is_gen {
let handler_block = self.new_block();
self.insert_cpython_stopiteration_setup_cleanup(handler_block);
self.push_fblock_labels(
FBlockType::StopIteration,
start_label,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)?;
Some(handler_block)
} else {
None
};
// Compile body statements
self.compile_statements(body)?;
if stop_iteration_block.is_some() {
self.emit_return_const_no_location(ConstantData::None);
}
// Close StopIteration handler and emit handler code
if let Some(handler_block) = stop_iteration_block {
self.pop_fblock_label(FBlockType::StopIteration, start_label);
self.use_cpython_label_block(handler_block);
emit!(
self,
Instruction::CallIntrinsic1 {
func: oparg::IntrinsicFunction1::StopIterationError
}
);
self.set_no_location();
emit!(self, Instruction::Reraise { depth: 1u32 });
self.set_no_location();
}
self.emit_return_const_no_location(ConstantData::None);
// Exit scope and create function object
let code = self.exit_scope();
self.ctx = prev_ctx;
self.set_source_range(closure_range);
// Create function object with closure
self.make_closure(code, funcflags)?;
// Note: docstring is now retrieved from co_consts[0] by the VM
// when HAS_DOCSTRING flag is set, so no runtime __doc__ assignment needed
Ok(())
}
/// Compile function annotations as a closure (PEP 649)
/// Returns true if an __annotate__ closure was created
/// Uses the matching annotation symbol table for proper scoping.
fn compile_annotations_closure(
&mut self,
func_name: &str,
parameters: &ast::Parameters,
returns: Option<&ast::Expr>,
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);
}
// Try to enter annotation scope - returns None if no matching symbol table exists.
let Some(saved_ctx) = self.enter_annotation_scope(func_name, func_range)? else {
return Ok(false);
};
// Count annotations
let parameters_iter = parameters
.args
.iter()
.map(|x| &x.parameter)
.chain(parameters.posonlyargs.iter().map(|x| &x.parameter))
.chain(parameters.vararg.as_deref())
.chain(parameters.kwonlyargs.iter().map(|x| &x.parameter))
.chain(parameters.kwarg.as_deref());
let num_annotations =
u32::try_from(parameters_iter.filter(|p| p.annotation.is_some()).count())
.expect("too many annotations")
+ u32::from(returns.is_some());
// Compile annotations inside the annotation scope
let parameters_iter = parameters
.args
.iter()
.map(|x| &x.parameter)
.chain(parameters.posonlyargs.iter().map(|x| &x.parameter))
.chain(parameters.vararg.as_deref())
.chain(parameters.kwonlyargs.iter().map(|x| &x.parameter))
.chain(parameters.kwarg.as_deref());
for param in parameters_iter {
if let Some(annotation) = &param.annotation {
self.set_source_range(func_range);
self.emit_load_const(ConstantData::Str {
value: self.mangle(param.name.id()).as_str().into(),
});
self.compile_annotation(annotation)?;
}
}
// Handle return annotation
if let Some(annotation) = returns {
self.set_source_range(func_range);
self.emit_load_const(ConstantData::Str {
value: "return".into(),
});
self.compile_annotation(annotation)?;
}
// Build the map and return it
self.set_source_range(func_range);
emit!(
self,
Instruction::BuildMap {
count: num_annotations,
}
);
emit!(self, Instruction::ReturnValue);
self.emit_return_const_no_location(ConstantData::None);
// Exit the annotation scope and get the code object
let annotate_code = self.exit_annotation_scope(saved_ctx);
// Make a closure from the code object
self.set_source_range(func_range);
self.make_closure(annotate_code, bytecode::MakeFunctionFlags::new())?;
Ok(true)
}
/// Collect annotated assignments from module/class body in AST order
/// (including nested conditional blocks). This preserves the same walk
/// order as symbol-table construction so the annotation scope's
/// `sub_tables` cursor stays aligned.
fn collect_annotations(
body: &[ast::Stmt],
parent_scope_type: CompilerScope,
) -> Vec<(&ast::StmtAnnAssign, bool)> {
fn walk<'a>(
stmts: &'a [ast::Stmt],
out: &mut Vec<(&'a ast::StmtAnnAssign, bool)>,
in_conditional_block: bool,
module_scope: bool,
) {
for stmt in stmts {
match stmt {
ast::Stmt::AnnAssign(stmt) => {
out.push((stmt, module_scope || in_conditional_block));
}
ast::Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
walk(body, out, true, module_scope);
for clause in elif_else_clauses {
walk(&clause.body, out, true, module_scope);
}
}
ast::Stmt::For(ast::StmtFor { body, orelse, .. })
| ast::Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
walk(body, out, true, module_scope);
walk(orelse, out, true, module_scope);
}
ast::Stmt::With(ast::StmtWith { body, .. }) => {
walk(body, out, true, module_scope);
}
ast::Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
}) => {
walk(body, out, true, module_scope);
for handler in handlers {
let ast::ExceptHandler::ExceptHandler(
ast::ExceptHandlerExceptHandler { body, .. },
) = handler;
walk(body, out, true, module_scope);
}
walk(orelse, out, true, module_scope);
walk(finalbody, out, true, module_scope);
}
ast::Stmt::Match(ast::StmtMatch { cases, .. }) => {
for case in cases {
walk(&case.body, out, true, module_scope);
}
}
_ => {}
}
}
}
let mut annotations = Vec::new();
walk(
body,
&mut annotations,
false,
parent_scope_type == CompilerScope::Module,
);
annotations
}
fn compile_annotation_for_symbol_cursor_only(
&mut self,
annotation: &ast::Expr,
) -> CompileResult<()> {
self.consume_skipped_nested_scopes_in_expr(annotation)
}
/// Compile module-level __annotate__ function (PEP 649)
/// Returns true if __annotate__ was created and stored
fn compile_module_annotate(
&mut self,
body: &[ast::Stmt],
loc: Option<TextRange>,
) -> CompileResult<bool> {
let loc = loc.unwrap_or(self.current_source_range);
// Get parent scope type BEFORE pushing annotation symbol table.
let parent_scope_type = self.current_symbol_table().typ;
let annotations = Self::collect_annotations(body, parent_scope_type);
let simple_annotation_count = annotations
.iter()
.filter(|(stmt, _)| stmt.simple && matches!(stmt.target.as_ref(), ast::Expr::Name(_)))
.count();
if simple_annotation_count == 0 {
return Ok(false);
}
// Check if we have conditional annotations
let has_conditional = self.current_symbol_table().has_conditional_annotations;
// Try to push annotation symbol table from current scope
if !self.push_current_annotation_symbol_table() {
return Ok(false);
}
// Annotation scopes are never async (even inside async functions)
let saved_ctx = self.ctx;
self.ctx = CompileContext {
in_class: saved_ctx.in_class,
func: FunctionContext::Function,
in_async_scope: false,
};
// Enter annotation scope for code generation
self.set_source_range(loc);
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get();
self.enter_scope(
"__annotate__",
CompilerScope::Annotation,
key,
lineno.to_u32(),
)?;
// Keep the internal ".format" name; the final code object
// exposes this parameter as "format".
self.configure_annotation_format_parameter();
// Emit format validation: if format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError
self.emit_format_validation();
self.set_source_range(loc);
emit!(self, Instruction::BuildMap { count: 0 });
let mut conditional_idx = 0usize;
for (stmt, is_conditional) in annotations {
let ast::StmtAnnAssign {
target,
annotation,
simple,
range,
..
} = stmt;
let simple_name = if *simple {
match target.as_ref() {
ast::Expr::Name(ast::ExprName { id, .. }) => Some(id),
_ => None,
}
} else {
None
};
if simple_name.is_none() {
if !self.future_annotations {
self.compile_annotation_for_symbol_cursor_only(annotation)?;
}
continue;
}
let not_set_block = (has_conditional && is_conditional).then(|| self.new_block());
let not_set_label = (!has_conditional || !is_conditional)
.then(|| self.current_code_info().new_instr_sequence_label());
let name = simple_name.expect("missing simple annotation name");
if let Some(not_set_block) = not_set_block {
self.set_source_range(*range);
self.emit_load_const(ConstantData::Integer {
value: conditional_idx.into(),
});
conditional_idx += 1;
if parent_scope_type == CompilerScope::Class {
let idx = self.get_free_var_index("__conditional_annotations__");
emit!(self, Instruction::LoadDeref { i: idx });
} else {
let cond_annotations_name = self.name("__conditional_annotations__");
self.emit_load_global(cond_annotations_name, false);
}
emit!(
self,
Instruction::ContainsOp {
invert: bytecode::Invert::No
}
);
emit!(
self,
Instruction::PopJumpIfFalse {
delta: not_set_block
}
);
}
self.compile_annotation(annotation)?;
self.set_source_range(*range);
emit!(self, Instruction::Copy { i: 2 });
self.emit_load_const(ConstantData::Str {
value: self.mangle(name).as_str().into(),
});
self.set_source_range(loc);
emit!(self, Instruction::StoreSubscr);
if let Some(not_set_block) = not_set_block {
self.use_cpython_label_block(not_set_block);
} else if let Some(not_set_label) = not_set_label {
let result = self
.current_code_info()
.use_raw_instr_sequence_label(not_set_label);
unwrap_internal(self, result);
}
}
self.set_source_range(loc);
emit!(self, Instruction::ReturnValue);
self.emit_return_const_no_location(ConstantData::None);
// Exit annotation scope - pop symbol table, restore to parent's annotation_block, and get code
let annotation_table = self.pop_symbol_table();
// Restore annotation_block to module's symbol table
self.symbol_table_stack
.last_mut()
.expect("no module symbol table")
.annotation_block = Some(Box::new(annotation_table));
// Restore context
self.ctx = saved_ctx;
// Exit code scope
let pop = self.code_stack.pop();
let mut annotate_code = unwrap_internal(
self,
compiler_unwrap_option(self, pop).finalize_code(&self.opts),
);
Self::expose_annotation_format_parameter(&mut annotate_code);
// Make a closure from the code object
self.set_source_range(loc);
self.make_closure(annotate_code, bytecode::MakeFunctionFlags::new())?;
// Store as __annotate_func__ for classes, __annotate__ for modules
let name = if parent_scope_type == CompilerScope::Class {
"__annotate_func__"
} else {
"__annotate__"
}
.into();
self.set_source_range(loc);
self.store_name(&name)?;
Ok(true)
}
// = compiler_function
#[expect(clippy::too_many_arguments, reason = "ignore warning for now")]
fn compile_function_def(
&mut self,
name: &Name,
parameters: &ast::Parameters,
body: &[ast::Stmt],
decorator_list: &[ast::Decorator],
returns: Option<&ast::Expr>, // TODO: use type hint somehow..
is_async: bool,
type_params: Option<&ast::TypeParams>,
preserve_value_before_store: bool,
) -> CompileResult<()> {
// CPython's FunctionDef/AsyncFunctionDef LOC(s) starts at the
// definition line even when decorators are present.
let stmt_source_range = self.current_source_range;
let def_source_range = crate::decorated_definition_range(
&self.source_file,
stmt_source_range,
decorator_list,
if is_async { "async def " } else { "def " },
);
// The symtable visits defaults before decorators, but
// codegen_function() emits decorators first. Keep the symbol table in
// symtable order and only move the codegen cursor while compiling
// decorators.
let defaults_symbol_table_cursors = self.current_symbol_table_cursors();
self.consume_skipped_nested_scopes_in_parameter_defaults(parameters)?;
self.prepare_decorators(decorator_list)?;
let after_decorators_symbol_table_cursors = self.current_symbol_table_cursors();
self.set_symbol_table_cursors(defaults_symbol_table_cursors);
// The first decorator line is used for code objects created by
// this definition, but LOC(s) for the surrounding instructions.
let firstlineno_range = decorator_list
.first()
.map_or(stmt_source_range, |decorator| decorator.expression.range());
// compile defaults and return funcflags
let funcflags = self.compile_default_arguments(parameters, def_source_range)?;
self.set_symbol_table_cursors(after_decorators_symbol_table_cursors);
let is_generic = type_params.is_some();
let mut num_typeparam_args = 0u32;
// Save context before entering TypeParams scope
let saved_ctx = self.ctx;
if is_generic {
// Count args to pass to type params scope
if funcflags.contains(&bytecode::MakeFunctionFlag::Defaults) {
num_typeparam_args += 1;
}
if funcflags.contains(&bytecode::MakeFunctionFlag::KwOnlyDefaults) {
num_typeparam_args += 1;
}
if num_typeparam_args == 2 {
self.set_source_range(def_source_range);
emit!(self, Instruction::Swap { i: 2 });
}
// Enter type params scope
let type_params_name = format!("<generic parameters of {name}>");
self.set_source_range(firstlineno_range);
self.push_output(
bytecode::CodeFlags::OPTIMIZED | bytecode::CodeFlags::NEWLOCALS,
0,
num_typeparam_args,
0,
&type_params_name,
)?;
// TypeParams scope is function-like
self.ctx = CompileContext {
in_class: saved_ctx.in_class,
func: FunctionContext::Function,
in_async_scope: false,
};
// Add parameter names to varnames for the type params scope
// These will be passed as arguments when the closure is called.
// `.defaults` is there whether or not the function has any: the
// symbol table gives every generic function's type params scope
// one. `.kwdefaults` only appears when it is really passed.
let current_info = self.current_code_info();
current_info
.metadata
.varnames
.insert(".defaults".to_owned());
if funcflags.contains(&bytecode::MakeFunctionFlag::KwOnlyDefaults) {
current_info
.metadata
.varnames
.insert(".kwdefaults".to_owned());
}
// Compile type parameters
self.compile_type_params(type_params.unwrap())?;
// Load defaults/kwdefaults with LOAD_FAST
self.set_source_range(def_source_range);
for i in 0..num_typeparam_args {
let var_num = oparg::VarNum::from(i);
emit!(self, Instruction::LoadFast { var_num });
}
}
// Compile annotations as closure (PEP 649)
let mut annotations_flag = bytecode::MakeFunctionFlags::new();
if self.compile_annotations_closure(name, parameters, returns, def_source_range)? {
annotations_flag.insert(bytecode::MakeFunctionFlag::Annotate);
}
// Compile function body. codegen_function() uses the first
// decorator line for co_firstlineno, but LOC(s) for MAKE_FUNCTION.
self.set_source_range(firstlineno_range);
let final_funcflags = funcflags | annotations_flag;
self.compile_function_body(
name,
parameters,
body,
is_async,
final_funcflags,
def_source_range,
)?;
// Handle type params if present
if is_generic {
// SWAP to get function on top
// Stack: [type_params_tuple, function] -> [function, type_params_tuple]
self.set_source_range(def_source_range);
emit!(self, Instruction::Swap { i: 2 });
// Call INTRINSIC_SET_FUNCTION_TYPE_PARAMS
self.set_source_range(def_source_range);
emit!(
self,
Instruction::CallIntrinsic2 {
func: bytecode::IntrinsicFunction2::SetFunctionTypeParams,
}
);
// Return the function object from type params scope
emit!(self, Instruction::ReturnValue);
self.set_no_location();
// Set argcount for type params scope
self.current_code_info().metadata.argcount = num_typeparam_args;
self.current_code_info().nparams = num_typeparam_args as usize;
// Exit type params scope and create closure
let type_params_code = self.exit_scope();
self.ctx = saved_ctx;
// Make closure for type params code
self.set_source_range(def_source_range);
self.make_closure(type_params_code, bytecode::MakeFunctionFlags::new())?;
if num_typeparam_args > 0 {
self.set_source_range(def_source_range);
emit!(
self,
Instruction::Swap {
i: num_typeparam_args + 1
}
);
self.set_source_range(def_source_range);
emit!(
self,
Instruction::Call {
argc: num_typeparam_args - 1
}
);
} else {
// Stack: [closure]
self.set_source_range(def_source_range);
emit!(self, Instruction::PushNull);
// Stack: [closure, NULL]
self.set_source_range(def_source_range);
emit!(self, Instruction::Call { argc: 0 });
}
}
// Apply decorators
self.apply_decorators(decorator_list);
// Store the function
self.set_source_range(def_source_range);
if preserve_value_before_store {
emit!(self, Instruction::Copy { i: 1 });
}
self.store_name(name)?;
Ok(())
}
/// Determines if a variable should be CELL or FREE type
// = get_ref_type
fn get_ref_type(&self, name: &Name) -> Result<SymbolScope, CodegenErrorType> {
let table = self.symbol_table_stack.last().unwrap();
// Special handling for __class__, __classdict__, and __conditional_annotations__ in class scope
// This should only apply when we're actually IN a class body,
// not when we're in a method nested inside a class.
if table.typ == CompilerScope::Class
&& (name == "__class__"
|| name == "__classdict__"
|| name == "__conditional_annotations__")
{
return Ok(SymbolScope::Cell);
}
match table.lookup(name) {
Some(symbol) => match symbol.scope {
SymbolScope::Cell => Ok(SymbolScope::Cell),
SymbolScope::Free => Ok(SymbolScope::Free),
_ if symbol.flags.contains(SymbolFlags::DEF_FREE_CLASS) => Ok(SymbolScope::Free),
_ => Err(CodegenErrorType::SyntaxError(format!(
"get_ref_type: invalid scope for '{name}'"
))),
},
None => Err(CodegenErrorType::SyntaxError(format!(
"get_ref_type: cannot find symbol '{name}'"
))),
}
}
/// Loads closure variables if needed and creates a function object
// = compiler_make_closure
fn make_closure(
&mut self,
code: CodeObject,
flags: bytecode::MakeFunctionFlags,
) -> CompileResult<()> {
// Handle free variables (closure)
let has_freevars = !code.freevars.is_empty();
if has_freevars {
// Build closure tuple by loading free variables
for var in &code.freevars {
// Special case: If a class contains a method with a
// free variable that has the same name as a method,
// the name will be considered free *and* local in the
// class. It should be handled by the closure, as
// well as by the normal name lookup logic.
// Get reference type using our get_ref_type function
let ref_type = self
.get_ref_type(&var.as_str().into())
.map_err(|e| self.error(e))?;
// Get parent code info
let parent_code = self.code_stack.last().unwrap();
let cellvars_len = parent_code.metadata.cellvars.len();
// Look up the variable index based on reference type
let idx = match ref_type {
SymbolScope::Cell => parent_code
.metadata
.cellvars
.get_index_of(var)
.or_else(|| {
parent_code
.metadata
.freevars
.get_index_of(var)
.map(|i| i + cellvars_len)
})
.ok_or_else(|| {
self.error(CodegenErrorType::SyntaxError(format!(
"compiler_make_closure: cannot find '{var}' in parent vars",
)))
})?,
SymbolScope::Free => parent_code
.metadata
.freevars
.get_index_of(var)
.map(|i| i + cellvars_len)
.or_else(|| parent_code.metadata.cellvars.get_index_of(var))
.ok_or_else(|| {
self.error(CodegenErrorType::SyntaxError(format!(
"compiler_make_closure: cannot find '{var}' in parent vars",
)))
})?,
_ => {
return Err(self.error(CodegenErrorType::SyntaxError(format!(
"compiler_make_closure: unexpected ref_type {ref_type:?} for '{var}'",
))));
}
};
emit!(self, PseudoInstruction::LoadClosure { i: idx.to_u32() });
}
// Build tuple of closure variables
emit!(
self,
Instruction::BuildTuple {
count: code.freevars.len().to_u32(),
}
);
}
// load code object and create function
self.emit_load_const(ConstantData::Code {
code: Box::new(code),
});
// Create function with no flags
emit!(self, Instruction::MakeFunction);
// Now set attributes one by one using SET_FUNCTION_ATTRIBUTE
// Note: The order matters! Values must be on stack before calling SET_FUNCTION_ATTRIBUTE
// Set closure if needed
if has_freevars {
emit!(
self,
Instruction::SetFunctionAttribute {
flag: bytecode::MakeFunctionFlag::Closure
}
);
}
// Set annotations if present
if flags.contains(&bytecode::MakeFunctionFlag::Annotations) {
emit!(
self,
Instruction::SetFunctionAttribute {
flag: bytecode::MakeFunctionFlag::Annotations
}
);
}
// Set __annotate__ closure if present (PEP 649)
if flags.contains(&bytecode::MakeFunctionFlag::Annotate) {
emit!(
self,
Instruction::SetFunctionAttribute {
flag: bytecode::MakeFunctionFlag::Annotate
}
);
}
// Set kwdefaults if present
if flags.contains(&bytecode::MakeFunctionFlag::KwOnlyDefaults) {
emit!(
self,
Instruction::SetFunctionAttribute {
flag: bytecode::MakeFunctionFlag::KwOnlyDefaults
}
);
}
// Set defaults if present
if flags.contains(&bytecode::MakeFunctionFlag::Defaults) {
emit!(
self,
Instruction::SetFunctionAttribute {
flag: bytecode::MakeFunctionFlag::Defaults
}
);
}
Ok(())
}
// Python/compile.c _PyCompile_MaybeAddStaticAttributeToClass
fn maybe_add_static_attribute_to_class(&mut self, value: &ast::Expr, attr: &str) {
if !matches!(value, ast::Expr::Name(n) if n.id.as_str() == "self") {
return;
}
if let Some(class_unit) = self
.code_stack
.iter_mut()
.rev()
.find(|unit| unit.static_attributes.is_some())
{
class_unit
.static_attributes
.as_mut()
.unwrap()
.insert(attr.to_owned());
}
}
/// Compile the class body into a code object
// = compiler_class_body
fn compile_class_body(
&mut self,
name: &str,
body: &[ast::Stmt],
type_params: Option<&ast::TypeParams>,
firstlineno: u32,
) -> CompileResult<CodeObject> {
// 1. Enter class scope
let key = self.symbol_table_stack.len();
self.push_symbol_table_matching(CompilerScope::Class, name)?;
self.enter_scope(name, CompilerScope::Class, key, firstlineno)?;
// Set qualname using the new method
let qualname = self.set_qualname();
// For class scopes, set u_private to the class name for name mangling
self.code_stack.last_mut().unwrap().private = Some(name.to_owned());
// 2. Set up class namespace
let (doc_str, body) = split_doc_with_range(body, &self.opts);
let class_body_prefix_range = self.source_line_start_range(firstlineno);
self.set_source_range(class_body_prefix_range);
// Load __name__ and store as __module__
self.load_name(&"__name__".into())?;
self.store_name(&"__module__".into())?;
// Store __qualname__
self.emit_load_const(ConstantData::Str {
value: qualname.into(),
});
self.store_name(&"__qualname__".into())?;
// Store __firstlineno__ before __doc__
self.emit_load_const(ConstantData::Integer {
value: BigInt::from(firstlineno),
});
self.store_name(&"__firstlineno__".into())?;
// Set __type_params__ from the enclosing type-params closure when
// compiling a generic class body.
if type_params.is_some() {
self.load_name(&".type_params".into())?;
self.store_name(&"__type_params__".into())?;
}
// PEP 649: Initialize __classdict__ after synthetic generic-class
// setup so nested generic classes match CPython's prologue order.
if self.current_symbol_table().needs_classdict {
emit!(self, Instruction::LoadLocals);
let classdict_idx = self.get_cell_var_index("__classdict__");
emit!(self, Instruction::StoreDeref { i: classdict_idx });
}
// Handle class annotation bookkeeping in CPython order.
let annotations_used = self.current_symbol_table().annotations_used;
if Self::scope_needs_conditional_annotations_cell(self.current_symbol_table()) {
emit!(self, Instruction::BuildSet { count: 0 });
self.store_name(&"__conditional_annotations__".into())?;
}
if self.future_annotations && annotations_used {
emit!(self, Instruction::SetupAnnotations);
}
// Store __doc__ only if there's an explicit docstring.
if let Some((doc, range)) = doc_str {
let saved_range = self.current_source_range;
self.set_source_range(range);
self.emit_load_const(ConstantData::Str { value: doc.into() });
self.store_name(&"__doc__".into())?;
self.set_no_location();
self.set_source_range(saved_range);
}
// 3. Compile the class body
self.compile_statements(body)?;
if annotations_used && !self.future_annotations {
self.compile_module_annotate(body, Some(class_body_prefix_range))?;
}
// 4. Handle __classcell__ if needed
let classcell_idx = self
.code_stack
.last_mut()
.unwrap()
.metadata
.cellvars
.iter()
.position(|var| *var == "__class__");
// Emit __static_attributes__ tuple
{
let mut attrs: Vec<String> = self
.code_stack
.last()
.unwrap()
.static_attributes
.as_ref()
.map(|s| s.iter().cloned().collect())
.unwrap_or_default();
attrs.sort();
self.emit_load_const(ConstantData::Tuple {
elements: attrs
.into_iter()
.map(|s| ConstantData::Str { value: s.into() })
.collect(),
});
self.set_no_location();
self.store_name(&"__static_attributes__".into())?;
self.set_no_location();
}
// Store __classdictcell__ if __classdict__ is a cell variable
if self.current_symbol_table().needs_classdict {
let classdict_idx = u32::from(self.get_cell_var_index("__classdict__"));
emit!(self, PseudoInstruction::LoadClosure { i: classdict_idx });
self.set_no_location();
self.store_name(&"__classdictcell__".into())?;
self.set_no_location();
}
if let Some(classcell_idx) = classcell_idx {
emit!(
self,
PseudoInstruction::LoadClosure {
i: classcell_idx.to_u32()
}
);
self.set_no_location();
emit!(self, Instruction::Copy { i: 1 });
self.set_no_location();
self.store_name(&"__classcell__".into())?;
self.set_no_location();
} else {
self.emit_load_const(ConstantData::None);
self.set_no_location();
}
// Return the class namespace
self.emit_return_value();
self.set_no_location();
self.emit_return_const_no_location(ConstantData::None);
// Exit scope and return the code object
Ok(self.exit_scope())
}
fn compile_class_def(
&mut self,
name: &Name,
body: &[ast::Stmt],
decorator_list: &[ast::Decorator],
type_params: Option<&ast::TypeParams>,
arguments: Option<&ast::Arguments>,
preserve_value_before_store: bool,
) -> CompileResult<()> {
// CPython's ClassDef LOC(s) starts at the class line even when
// decorators are present.
let stmt_source_range = self.current_source_range;
let class_source_range = crate::decorated_definition_range(
&self.source_file,
stmt_source_range,
decorator_list,
"class ",
);
self.prepare_decorators(decorator_list)?;
let is_generic = type_params.is_some();
let firstlineno_range = decorator_list
.first()
.map_or(stmt_source_range, |decorator| decorator.expression.range());
#[expect(clippy::map_unwrap_or, reason = "Changing this will not compile")]
let firstlineno = decorator_list
.first()
.map(|decorator| {
self.source_file
.to_source_code()
.line_index(decorator.expression.range().start())
.get()
.to_u32()
})
.unwrap_or_else(|| self.get_source_line_number().get().to_u32());
// Save context before entering any scopes
let saved_ctx = self.ctx;
// Step 1: If generic, enter type params scope and compile type params
if is_generic {
let type_params_name = format!("<generic parameters of {name}>");
self.set_source_range(firstlineno_range);
self.push_output(
bytecode::CodeFlags::OPTIMIZED | bytecode::CodeFlags::NEWLOCALS,
0,
0,
0,
&type_params_name,
)?;
// Set private name for name mangling
self.code_stack.last_mut().unwrap().private = Some(name.as_str().to_owned());
// TypeParams scope is function-like
self.ctx = CompileContext {
in_class: saved_ctx.in_class,
func: FunctionContext::Function,
in_async_scope: false,
};
// Compile type parameters and store them in the synthetic cell that
// generic class bodies close over.
self.compile_type_params(type_params.unwrap())?;
self.set_source_range(class_source_range);
self.store_name(&".type_params".into())?;
}
// Step 2: Compile class body (always done, whether generic or not)
let prev_ctx = self.ctx;
self.ctx = CompileContext {
func: FunctionContext::NoFunction,
in_class: true,
in_async_scope: false,
};
let pre_class_body_symbol_table_cursors = self.current_symbol_table_cursors();
let class_code = self.compile_class_body(name, body, type_params, firstlineno)?;
let post_class_body_symbol_table_cursors = self.current_symbol_table_cursors();
self.set_symbol_table_cursors(pre_class_body_symbol_table_cursors);
self.ctx = prev_ctx;
self.set_source_range(class_source_range);
// Step 3: Generate the rest of the code for the call
if is_generic {
// Generate class creation code
emit!(self, Instruction::LoadBuildClass);
emit!(self, Instruction::PushNull);
// Create the class body function with the .type_params closure
// captured through the class code object's freevars.
self.make_closure(class_code, bytecode::MakeFunctionFlags::new())?;
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});
// Create .generic_base after the class function and name are on the
// stack so the remaining call shape matches CPython's ordering.
self.set_source_range(class_source_range);
self.load_name(&".type_params".into())?;
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::SubscriptGeneric
}
);
self.set_source_range(class_source_range);
self.store_name(&".generic_base".into())?;
let (bases, keywords) = arguments.map_or((&[][..], &[][..]), |args| {
(&args.args[..], &args.keywords[..])
});
self.codegen_call_helper_impl(
2,
bases,
keywords,
class_source_range,
None,
Some(&".generic_base".into()),
)?;
// Return the created class
self.emit_return_value();
self.set_no_location();
// Exit type params scope and wrap in function
let type_params_code = self.exit_scope();
self.ctx = saved_ctx;
// Execute the type params function
self.set_source_range(class_source_range);
self.make_closure(type_params_code, bytecode::MakeFunctionFlags::new())?;
self.set_source_range(class_source_range);
emit!(self, Instruction::PushNull);
self.set_source_range(class_source_range);
emit!(self, Instruction::Call { argc: 0 });
} else {
// Non-generic class: standard path
emit!(self, Instruction::LoadBuildClass);
emit!(self, Instruction::PushNull);
// Create class function with closure
self.make_closure(class_code, bytecode::MakeFunctionFlags::new())?;
self.emit_load_const(ConstantData::Str {
value: name.as_str().into(),
});
if let Some(arguments) = arguments {
self.codegen_call_helper(2, arguments, class_source_range, None)?;
} else {
self.set_source_range(class_source_range);
emit!(self, Instruction::Call { argc: 2 });
}
self.set_symbol_table_cursors(post_class_body_symbol_table_cursors);
}
// Step 4: Apply decorators and store (common to both paths)
self.apply_decorators(decorator_list);
self.set_source_range(class_source_range);
if preserve_value_before_store {
emit!(self, Instruction::Copy { i: 1 });
}
self.store_name(name)
}
/// Compile an if statement with constant condition elimination.
/// = compiler_if in CPython codegen.c
fn compile_if(
&mut self,
test: &ast::Expr,
body: &[ast::Stmt],
elif_else_clauses: &[ast::ElifElseClause],
stmt_range: TextRange,
) -> CompileResult<()> {
let end_block = self.new_block();
let next_block = if elif_else_clauses.is_empty() {
end_block
} else {
self.new_block()
};
self.compile_jump_if_inner(test, false, next_block, Some(stmt_range))?;
self.compile_statements(body)?;
let Some((clause, rest)) = elif_else_clauses.split_first() else {
self.use_cpython_label_block(end_block);
return Ok(());
};
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: end_block }
);
self.set_no_location();
self.use_cpython_label_block(next_block);
if let Some(test) = &clause.test {
self.compile_if(test, &clause.body, rest, clause.range)?;
} else {
debug_assert!(rest.is_empty());
self.compile_statements(&clause.body)?;
}
self.use_cpython_label_block(end_block);
Ok(())
}
fn compile_while(
&mut self,
test: &ast::Expr,
body: &[ast::Stmt],
orelse: &[ast::Stmt],
while_range: TextRange,
) -> CompileResult<()> {
self.enter_conditional_block();
let loop_block = self.new_block();
let end_block = self.new_block();
let anchor_block = self.new_block();
let loop_label = self.instr_sequence_label_for_block(loop_block);
let end_label = self.instr_sequence_label_for_block(end_block);
self.use_cpython_label_block(loop_block);
self.push_fblock_labels(
FBlockType::WhileLoop,
loop_label,
end_label,
FBlockDatum::None,
)?;
self.compile_jump_if_inner(test, false, anchor_block, Some(while_range))?;
self.compile_loop_body_statements(body)?;
emit!(self, PseudoInstruction::Jump { delta: loop_block });
self.set_no_location();
self.pop_fblock_label(FBlockType::WhileLoop, loop_label);
self.use_cpython_label_block(anchor_block);
self.compile_statements(orelse)?;
self.use_cpython_label_block(end_block);
self.leave_conditional_block();
Ok(())
}
fn compile_with(
&mut self,
items: &[ast::WithItem],
body: &[ast::Stmt],
is_async: bool,
) -> CompileResult<()> {
self.enter_conditional_block();
let result = self.compile_with_inner(items, body, is_async);
self.leave_conditional_block();
result
}
fn compile_with_inner(
&mut self,
items: &[ast::WithItem],
body: &[ast::Stmt],
is_async: bool,
) -> CompileResult<()> {
// Python 3.12+ style with statement:
//
// BEFORE_WITH # TOS: ctx_mgr -> [__exit__, __enter__ result]
// L1: STORE_NAME f # exception table: L1 to L2 -> L3 [1] lasti
// L2: ... body ...
// LOAD_CONST None # normal exit
// LOAD_CONST None
// LOAD_CONST None
// CALL 2 # __exit__(None, None, None)
// POP_TOP
// JUMP after
// L3: PUSH_EXC_INFO # exception handler
// WITH_EXCEPT_START # call __exit__(type, value, tb), push result
// TO_BOOL
// POP_JUMP_IF_TRUE suppress
// RERAISE 2
// suppress:
// POP_TOP # pop exit result
// L5: POP_EXCEPT
// POP_TOP # pop __exit__
// POP_TOP # pop prev_exc (or lasti depending on layout)
// JUMP after
// L6: COPY 3 # cleanup handler for reraise
// POP_EXCEPT
// RERAISE 1
// after: ...
let Some((item, items)) = items.split_first() else {
return Err(self.error(CodegenErrorType::EmptyWithItems));
};
let with_range = item.context_expr.range();
let body_block = self.new_block();
let exc_handler_block = self.new_block();
let after_block = self.new_block();
let cleanup_block = self.new_block();
// Compile context expression and load __enter__/__exit__ methods
self.compile_expression(&item.context_expr)?;
self.set_source_range(with_range);
// Stack: [cm]
emit!(self, Instruction::Copy { i: 1 }); // [cm, cm]
if is_async {
if self.ctx.func != FunctionContext::AsyncFunction
&& !self.allows_top_level_await_in_current_context()
{
return Err(self.error(CodegenErrorType::InvalidAsyncWith));
}
// Load __aexit__ and __aenter__, then call __aenter__
emit!(
self,
Instruction::LoadSpecial {
method: SpecialMethod::AExit
}
); // [cm, aexit_func, self_ae]
emit!(self, Instruction::Swap { i: 2 }); // [cm, self_ae, aexit_func]
emit!(self, Instruction::Swap { i: 3 }); // [aexit_func, self_ae, cm]
emit!(
self,
Instruction::LoadSpecial {
method: SpecialMethod::AEnter
}
); // [aexit_func, self_ae, aenter_func, self_an]
emit!(self, Instruction::Call { argc: 0 }); // [aexit_func, self_ae, awaitable]
emit!(self, Instruction::GetAwaitable { r#where: 1 });
self.emit_load_const(ConstantData::None);
let _ = self.compile_yield_from_sequence(true);
} else {
// Load __exit__ and __enter__, then call __enter__
emit!(
self,
Instruction::LoadSpecial {
method: SpecialMethod::Exit
}
); // [cm, exit_func, self_exit]
emit!(self, Instruction::Swap { i: 2 }); // [cm, self_exit, exit_func]
emit!(self, Instruction::Swap { i: 3 }); // [exit_func, self_exit, cm]
emit!(
self,
Instruction::LoadSpecial {
method: SpecialMethod::Enter
}
); // [exit_func, self_exit, enter_func, self_enter]
emit!(self, Instruction::Call { argc: 0 }); // [exit_func, self_exit, result]
}
// Stack: [..., __exit__, enter_result]
// Push fblock for exception table - handler goes to exc_handler_block
// preserve_lasti=true for with statements
emit!(
self,
PseudoInstruction::SetupWith {
delta: exc_handler_block
}
);
self.use_cpython_label_block(body_block);
let fblock_type = if is_async {
FBlockType::AsyncWith
} else {
FBlockType::With
};
let body_label = self.instr_sequence_label_for_block(body_block);
let exc_handler_label = self.instr_sequence_label_for_block(exc_handler_block);
self.push_fblock_labels(
fblock_type,
body_label,
exc_handler_label,
FBlockDatum::None,
)?;
// Store or pop the enter result
match &item.optional_vars {
Some(var) => {
self.set_source_range(var.range());
self.compile_store(var)?;
}
None => {
emit!(self, Instruction::PopTop);
}
}
// Stack: [..., __exit__]
// Compile body or nested with
if items.is_empty() {
if body.is_empty() {
return Err(self.error(CodegenErrorType::EmptyWithBody));
}
self.compile_with_body_statements(body)?;
} else {
self.set_source_range(items[0].context_expr.range());
self.compile_with_inner(items, body, is_async)?;
}
// CPython pops the async-with fblock before emitting POP_BLOCK, but
// sync with emits the artificial POP_BLOCK before popping the fblock.
if is_async {
self.pop_fblock_label(fblock_type, body_label);
self.set_source_range(with_range);
emit!(self, PseudoInstruction::PopBlock);
} else {
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
self.set_source_range(with_range);
self.pop_fblock_label(fblock_type, body_label);
}
self.compile_call_exit_with_nones();
if is_async {
emit!(self, Instruction::GetAwaitable { r#where: 2 });
self.emit_load_const(ConstantData::None);
let _ = self.compile_yield_from_sequence(true);
}
emit!(self, Instruction::PopTop); // Pop __exit__ result
emit!(self, PseudoInstruction::Jump { delta: after_block });
// ===== Exception handler path =====
// Stack at entry: [..., exit_func, self_exit, lasti, exc]
// PUSH_EXC_INFO -> [..., exit_func, self_exit, lasti, prev_exc, exc]
self.use_cpython_label_block(exc_handler_block);
emit!(
self,
PseudoInstruction::SetupCleanup {
delta: cleanup_block
}
);
emit!(self, Instruction::PushExcInfo);
// WITH_EXCEPT_START: call exit_func(self_exit, type, value, tb)
// Stack: [..., exit_func, self_exit, lasti, prev_exc, exc]
emit!(self, Instruction::WithExceptStart);
if is_async {
emit!(self, Instruction::GetAwaitable { r#where: 2 });
self.emit_load_const(ConstantData::None);
let _ = self.compile_yield_from_sequence(true);
}
self.compile_with_except_finish(cleanup_block);
self.use_cpython_label_block(after_block);
Ok(())
}
fn compile_for(
&mut self,
target: &ast::Expr,
iter: &ast::Expr,
body: &[ast::Stmt],
orelse: &[ast::Stmt],
is_async: bool,
for_range: TextRange,
) -> CompileResult<()> {
self.enter_conditional_block();
// Start loop
let for_block = self.new_block();
let (body_label, send_block) = if is_async {
(None, self.new_block())
} else {
let body_label = self.current_code_info().new_instr_sequence_label();
(Some(body_label), BlockIdx::NULL)
};
let else_block = self.new_block();
let after_block = self.new_block();
let for_label = self.instr_sequence_label_for_block(for_block);
let after_label = self.instr_sequence_label_for_block(after_block);
let mut end_async_for_target = BlockIdx::NULL;
if !is_async {
// CPython codegen_for() pushes the loop fblock before compiling
// the iterable expression.
self.push_fblock_labels(
FBlockType::ForLoop,
for_label,
after_label,
FBlockDatum::None,
)?;
}
// The thing iterated:
self.compile_expression(iter)?;
if is_async {
if self.ctx.func != FunctionContext::AsyncFunction
&& !self.allows_top_level_await_in_current_context()
{
return Err(self.error(CodegenErrorType::InvalidAsyncFor));
}
self.set_source_range(iter.range());
emit!(self, Instruction::GetAiter);
self.use_cpython_label_block(for_block);
self.set_source_range(for_range);
// codegen_async_for: push fblock BEFORE SETUP_FINALLY
self.push_fblock_labels(
FBlockType::ForLoop,
for_label,
after_label,
FBlockDatum::None,
)?;
// SETUP_FINALLY to guard the __anext__ call
emit!(self, PseudoInstruction::SetupFinally { delta: else_block });
emit!(self, Instruction::GetAnext);
self.emit_load_const(ConstantData::None);
self.use_cpython_label_block(send_block);
let _ = self.compile_yield_from_sequence(true);
end_async_for_target = send_block;
// POP_BLOCK for SETUP_FINALLY - only GetANext/yield_from are protected
emit!(self, PseudoInstruction::PopBlock);
emit!(self, Instruction::NotTaken);
// Success block for __anext__
self.compile_store(target)?;
} else {
// Retrieve Iterator
self.set_source_range(iter.range());
emit!(self, Instruction::GetIter);
self.use_cpython_label_block(for_block);
emit!(self, Instruction::ForIter { delta: else_block });
let saved_range = self.current_source_range;
self.set_source_range(target.range());
emit!(self, Instruction::Nop);
let result = self
.current_code_info()
.use_raw_instr_sequence_label(body_label.expect("sync for must have body label"));
unwrap_internal(self, result);
self.compile_store(target)?;
self.set_source_range(saved_range);
};
self.compile_loop_body_statements(body)?;
emit!(self, PseudoInstruction::Jump { delta: for_block });
self.set_no_location();
if is_async {
// codegen_async_for() pops the loop fblock before the
// END_ASYNC_FOR exception block. Sync codegen_for() keeps the
// fblock through END_FOR/POP_ITER and pops below.
self.pop_fblock_label(FBlockType::ForLoop, for_label);
}
self.use_cpython_label_block(else_block);
// Except block for __anext__ / end of sync for
if is_async {
// codegen_async_for emits END_ASYNC_FOR at the iterator location,
// after the for-loop fblock has already been popped.
let saved_range = self.current_source_range;
self.set_source_range(iter.range());
self.emit_end_async_for(end_async_for_target);
self.set_source_range(saved_range);
} else {
// codegen_for emits END_FOR/POP_ITER with NO_LOCATION. Line numbers
// are propagated later by flowgraph.c::resolve_line_numbers().
emit!(self, Instruction::EndFor);
self.set_no_location();
emit!(self, Instruction::PopIter);
self.set_no_location();
self.pop_fblock_label(FBlockType::ForLoop, for_label);
}
self.compile_statements(orelse)?;
self.use_cpython_label_block(after_block);
// Implicit return after for-loop should be attributed to the `for` line
self.set_source_range(iter.range());
self.leave_conditional_block();
Ok(())
}
fn compile_comprehension_iter(&mut self, generator: &ast::Comprehension) -> CompileResult<()> {
let saved_range = self.current_source_range;
self.compile_expression(&generator.iter)?;
self.set_source_range(generator.iter.range());
if generator.is_async {
emit!(self, Instruction::GetAiter);
} else {
emit!(self, Instruction::GetIter);
}
self.set_source_range(saved_range);
Ok(())
}
fn singleton_comprehension_assignment_iter(iter: &ast::Expr) -> Option<&ast::Expr> {
let elts = match iter {
ast::Expr::List(ast::ExprList { elts, .. }) => elts,
ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => elts,
_ => return None,
};
match elts.as_slice() {
[elt] if !matches!(elt, ast::Expr::Starred(_)) => Some(elt),
_ => None,
}
}
/// Ensures that `pc.fail_pop` has at least `n + 1` entries.
/// If not, new labels are generated and pushed until the required size is reached.
fn ensure_fail_pop(&mut self, pc: &mut PatternContext, n: usize) {
let required_size = n + 1;
if required_size <= pc.fail_pop.len() {
return;
}
while pc.fail_pop.len() < required_size {
let new_block = self.new_block();
pc.fail_pop.push(new_block);
}
}
fn jump_to_fail_pop(&mut self, pc: &mut PatternContext, op: JumpOp) {
// Compute the total number of items to pop:
// items on top plus the captured objects.
let pops = pc.on_top + pc.stores.len();
// Ensure that the fail_pop vector has at least `pops + 1` elements.
self.ensure_fail_pop(pc, pops);
// Emit a jump using the jump target stored at index `pops`.
match op {
JumpOp::Jump => {
emit!(
self,
PseudoInstruction::Jump {
delta: pc.fail_pop[pops]
}
)
}
JumpOp::PopJumpIfFalse => {
emit!(
self,
Instruction::PopJumpIfFalse {
delta: pc.fail_pop[pops]
}
)
}
};
}
/// Emits the necessary POP instructions for all failure targets in the pattern context,
/// then resets the fail_pop vector.
fn emit_and_reset_fail_pop(&mut self, pc: &mut PatternContext, loc: TextRange) {
// If the fail_pop vector is empty, nothing needs to be done.
if pc.fail_pop.is_empty() {
debug_assert!(pc.fail_pop.is_empty());
return;
}
// Iterate over the fail_pop vector in reverse order, skipping the first label.
for &label in pc.fail_pop.iter().skip(1).rev() {
// CPython emit_and_reset_fail_pop() uses USE_LABEL here.
self.use_cpython_label_block(label);
// Emit the POP instruction.
self.set_source_range(loc);
emit!(self, Instruction::PopTop);
}
// Finally, use the first label.
// CPython emit_and_reset_fail_pop() uses USE_LABEL here too.
self.use_cpython_label_block(pc.fail_pop[0]);
pc.fail_pop.clear();
// Free the memory used by the vector.
pc.fail_pop.shrink_to_fit();
}
/// Duplicate the effect of Python 3.10's ROT_* instructions using SWAPs.
fn pattern_helper_rotate(&mut self, loc: TextRange, mut count: usize) {
// Rotate TOS (top of stack) to position `count` down
// This is done by a series of swaps
// For count=1, no rotation needed (already at top)
// For count=2, swap TOS with item 1 position down
// For count=3, swap TOS with item 2 positions down, then with item 1 position down
while count > 1 {
// Emit a SWAP instruction with the current count.
self.set_source_range(loc);
emit!(
self,
Instruction::Swap {
i: u32::try_from(count).unwrap()
}
);
count -= 1;
}
}
/// Helper to store a captured name for a star pattern.
///
/// If `n` is `None`, it emits a POP_TOP instruction. Otherwise, it first
/// checks that the name is allowed and not already stored. Then it rotates
/// the object on the stack beneath any preserved items and appends the name
/// to the list of captured names.
fn pattern_helper_store_name(
&mut self,
loc: TextRange,
n: Option<&ast::Identifier>,
pc: &mut PatternContext,
) -> CompileResult<()> {
match n {
// If no name is provided, simply pop the top of the stack.
None => {
self.set_source_range(loc);
emit!(self, Instruction::PopTop);
Ok(())
}
Some(name) => {
// Ensure we don't store the same name twice.
// TODO: maybe pc.stores should be a set?
if pc.stores.contains(name.id()) {
return Err(self.error_ranged(
CodegenErrorType::DuplicateStore(name.as_str().to_string()),
loc,
));
}
// Calculate how many items to rotate:
let rotations = pc.on_top + pc.stores.len() + 1;
self.pattern_helper_rotate(loc, rotations);
// Append the name to the captured stores.
pc.stores.push(name.id().clone());
Ok(())
}
}
}
fn pattern_wildcard_check(pattern: &ast::Pattern) -> bool {
matches!(
pattern,
ast::Pattern::MatchAs(ast::PatternMatchAs { name: None, .. })
)
}
fn pattern_wildcard_star_check(pattern: &ast::Pattern) -> bool {
matches!(
pattern,
ast::Pattern::MatchStar(ast::PatternMatchStar { name: None, .. })
)
}
fn pattern_unpack_helper(
&mut self,
loc: TextRange,
elts: &[ast::Pattern],
) -> CompileResult<()> {
let n = elts.len();
let mut seen_star = false;
for (i, elt) in elts.iter().enumerate() {
if elt.is_match_star() && !seen_star {
if i >= (1 << 8) || (n - i - 1) >= ((i32::MAX as usize) >> 8) {
return Err(self.error_ranged(
CodegenErrorType::TooManyExpressionsInStarUnpackingSequencePattern,
loc,
));
}
let counts = UnpackExArgs {
before: u8::try_from(i).unwrap(),
after: u32::try_from(n - i - 1).unwrap(),
};
self.set_source_range(loc);
emit!(self, Instruction::UnpackEx { counts });
seen_star = true;
} else if elt.is_match_star() {
return Err(self.error_ranged(
CodegenErrorType::MultipleStarredExpressionsInSequencePattern,
loc,
));
}
}
if !seen_star {
self.set_source_range(loc);
emit!(
self,
Instruction::UnpackSequence {
count: u32::try_from(n).unwrap()
}
);
}
Ok(())
}
fn pattern_helper_sequence_unpack(
&mut self,
loc: TextRange,
patterns: &[ast::Pattern],
_star: Option<usize>,
pc: &mut PatternContext,
) -> CompileResult<()> {
// Unpack the sequence into individual subjects.
self.pattern_unpack_helper(loc, patterns)?;
let size = patterns.len();
// Increase the on_top counter for the newly unpacked subjects.
pc.on_top += size;
// For each unpacked subject, compile its subpattern.
for pattern in patterns {
// Decrement on_top for each subject as it is consumed.
pc.on_top -= 1;
self.compile_pattern_subpattern(pattern, pc)?;
}
Ok(())
}
fn pattern_helper_sequence_subscr(
&mut self,
loc: TextRange,
patterns: &[ast::Pattern],
star: usize,
pc: &mut PatternContext,
) -> CompileResult<()> {
// Keep the subject around for extracting elements.
pc.on_top += 1;
for (i, pattern) in patterns.iter().enumerate() {
if Self::pattern_wildcard_check(pattern) {
continue;
}
if i == star {
// This must be a starred wildcard.
debug_assert!(Self::pattern_wildcard_star_check(pattern));
continue;
}
// Duplicate the subject.
self.set_source_range(loc);
emit!(self, Instruction::Copy { i: 1 });
if i < star {
// For indices before the star, use a nonnegative index equal to i.
self.set_source_range(loc);
self.emit_load_const(ConstantData::Integer { value: i.into() });
} else {
// For indices after the star, compute a nonnegative index:
// index = len(subject) - (size - i)
self.set_source_range(loc);
emit!(self, Instruction::GetLen);
self.set_source_range(loc);
self.emit_load_const(ConstantData::Integer {
value: (patterns.len() - i).into(),
});
// Subtract to compute the correct index.
self.set_source_range(loc);
emit!(
self,
Instruction::BinaryOp {
op: BinaryOperator::Subtract
}
);
}
// Use BINARY_OP/NB_SUBSCR to extract the element.
self.set_source_range(loc);
emit!(
self,
Instruction::BinaryOp {
op: BinaryOperator::Subscr
}
);
// Compile the subpattern in irrefutable mode.
self.compile_pattern_subpattern(pattern, pc)?;
}
// Pop the subject off the stack.
pc.on_top -= 1;
self.set_source_range(loc);
emit!(self, Instruction::PopTop);
Ok(())
}
fn compile_pattern_subpattern(
&mut self,
p: &ast::Pattern,
pc: &mut PatternContext,
) -> CompileResult<()> {
// Save the current allow_irrefutable state.
let old_allow_irrefutable = pc.allow_irrefutable;
// Temporarily allow irrefutable patterns.
pc.allow_irrefutable = true;
// Compile the pattern.
self.compile_pattern(p, pc)?;
// Restore the original state.
pc.allow_irrefutable = old_allow_irrefutable;
Ok(())
}
fn compile_pattern_as(
&mut self,
p: &ast::PatternMatchAs,
pc: &mut PatternContext,
) -> CompileResult<()> {
// If there is no sub-pattern, then it's an irrefutable match.
if p.pattern.is_none() {
if !pc.allow_irrefutable {
if let Some(name) = p.name.as_ref() {
return Err(self.error_ranged(
CodegenErrorType::UnreachableNameCapturePattern(name.to_string()),
p.range,
));
}
// A wildcard makes remaining patterns unreachable.
return Err(
self.error_ranged(CodegenErrorType::UnreachableWildcardPattern, p.range)
);
}
// If irrefutable matches are allowed, store the name (if any).
return self.pattern_helper_store_name(p.range, p.name.as_ref(), pc);
}
// Otherwise, there is a sub-pattern. Duplicate the object on top of the stack.
pc.on_top += 1;
self.set_source_range(p.range);
emit!(self, Instruction::Copy { i: 1 });
// Compile the sub-pattern.
self.compile_pattern(p.pattern.as_ref().unwrap(), pc)?;
// After success, decrement the on_top counter.
pc.on_top -= 1;
// Store the captured name (if any).
self.pattern_helper_store_name(p.range, p.name.as_ref(), pc)?;
Ok(())
}
fn compile_pattern_star(
&mut self,
p: &ast::PatternMatchStar,
pc: &mut PatternContext,
) -> CompileResult<()> {
self.pattern_helper_store_name(p.range, p.name.as_ref(), pc)?;
Ok(())
}
/// Validates that keyword attributes in a class pattern are allowed
/// and not duplicated.
fn validate_kwd_attrs(
&mut self,
attrs: &[ast::Identifier],
patterns: &[ast::Pattern],
) -> CompileResult<()> {
let n_attrs = attrs.len();
for i in 0..n_attrs {
let attr = attrs[i].as_str();
// Check for duplicates: compare with every subsequent attribute.
for (j, ident) in attrs.iter().enumerate().take(n_attrs).skip(i + 1) {
let other = ident.as_str();
if attr == other {
return Err(self.error_ranged(
CodegenErrorType::RepeatedAttributePattern(attr.to_owned()),
patterns[j].range(),
));
}
}
}
Ok(())
}
fn compile_pattern_class(
&mut self,
p: &ast::PatternMatchClass,
pc: &mut PatternContext,
) -> CompileResult<()> {
// Extract components from the MatchClass pattern.
let match_class = p;
let patterns = &match_class.arguments.patterns;
// Extract keyword attributes and patterns.
// Capacity is pre-allocated based on the number of keyword arguments.
let mut kwd_attrs = Vec::with_capacity(match_class.arguments.keywords.len());
let mut kwd_patterns = Vec::with_capacity(match_class.arguments.keywords.len());
for kwd in &match_class.arguments.keywords {
kwd_attrs.push(kwd.attr.clone());
kwd_patterns.push(kwd.pattern.clone());
}
let nargs = patterns.len();
let n_attrs = kwd_attrs.len();
let n_kwd_patterns = kwd_patterns.len();
if n_attrs != n_kwd_patterns {
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(format!(
"kwd_attrs ({n_attrs}) / kwd_patterns ({n_kwd_patterns}) length mismatch in class pattern"
)),
p.range,
));
}
// Check for too many sub-patterns.
if nargs > i32::MAX as usize
|| nargs.saturating_add(n_attrs).saturating_sub(1) > i32::MAX as usize
{
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(format!(
"too many sub-patterns in class pattern {}",
UnparseExpr::new(&match_class.cls, &self.source_file)
)),
p.range,
));
}
// Validate keyword attributes if any.
if n_attrs != 0 {
self.validate_kwd_attrs(&kwd_attrs, &kwd_patterns)?;
}
// Compile the class expression.
self.compile_expression(&match_class.cls)?;
self.set_source_range(p.range);
// Create a new tuple of attribute names.
let mut attr_names = vec![];
for name in &kwd_attrs {
// Py_NewRef(name) is emulated by cloning the name into a PyObject.
attr_names.push(ConstantData::Str {
value: name.as_str().to_string().into(),
});
}
// Emit instructions:
// 1. Load the new tuple of attribute names.
self.emit_load_const(ConstantData::Tuple {
elements: attr_names,
});
// 2. Emit MATCH_CLASS with nargs.
emit!(
self,
Instruction::MatchClass {
count: u32::try_from(nargs).unwrap()
}
);
// 3. Duplicate the top of the stack.
emit!(self, Instruction::Copy { i: 1 });
// 4. Load None.
self.emit_load_const(ConstantData::None);
// 5. Compare with IS_OP 1.
emit!(
self,
Instruction::IsOp {
invert: Invert::Yes
}
);
// At this point the TOS is a tuple of (nargs + n_attrs) attributes (or None).
pc.on_top += 1;
self.set_source_range(p.range);
self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse);
// Unpack the tuple into (nargs + n_attrs) items.
let total = nargs + n_attrs;
emit!(
self,
Instruction::UnpackSequence {
count: u32::try_from(total).unwrap()
}
);
if total == 0 {
pc.on_top -= 1;
} else {
pc.on_top += total - 1;
}
// Process each sub-pattern.
for subpattern in patterns.iter().chain(kwd_patterns.iter()) {
// Decrement the on_top counter for each sub-pattern
pc.on_top -= 1;
if Self::pattern_wildcard_check(subpattern) {
self.set_source_range(p.range);
emit!(self, Instruction::PopTop);
continue; // Don't compile wildcard patterns
}
// Compile the subpattern without irrefutability checks.
self.compile_pattern_subpattern(subpattern, pc)?;
}
Ok(())
}
fn compile_pattern_mapping(
&mut self,
p: &ast::PatternMatchMapping,
pc: &mut PatternContext,
) -> CompileResult<()> {
let mapping = p;
let keys = &mapping.keys;
let patterns = &mapping.patterns;
let size = keys.len();
let star_target = &mapping.rest;
// Validate pattern count matches key count
if keys.len() != patterns.len() {
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(format!(
"keys ({}) / patterns ({}) length mismatch in mapping pattern",
keys.len(),
patterns.len()
)),
p.range,
));
}
// `case {**_}:` is rejected before codegen. RustPython's parser
// currently lets it through, so keep the compiler boundary equivalent.
if let Some(rest) = star_target
&& rest.as_str() == "_"
{
return Err(self.error_ranged(
CodegenErrorType::SyntaxError("invalid syntax".to_string()),
rest.range,
));
}
// Step 1: Check if subject is a mapping
// Stack: [subject]
pc.on_top += 1;
self.set_source_range(p.range);
emit!(self, Instruction::MatchMapping);
// Stack: [subject, is_mapping]
self.set_source_range(p.range);
self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse);
// Stack: [subject]
// Special case: empty pattern {} with no rest
if size == 0 && star_target.is_none() {
// If the pattern is just "{}", we're done! Pop the subject
pc.on_top -= 1;
emit!(self, Instruction::PopTop);
return Ok(());
}
// Length check for patterns with keys
if size > 0 {
// Check if the mapping has at least 'size' keys
self.set_source_range(p.range);
emit!(self, Instruction::GetLen);
self.set_source_range(p.range);
self.emit_load_const(ConstantData::Integer { value: size.into() });
// Stack: [subject, len, size]
self.set_source_range(p.range);
emit!(
self,
Instruction::CompareOp {
opname: ComparisonOperator::GreaterOrEqual
}
);
self.set_source_range(p.range);
self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse);
// Stack: [subject]
}
// Check for overflow (INT_MAX < size - 1)
if size.saturating_sub(1) > i32::MAX as usize {
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(
"too many sub-patterns in mapping pattern".to_string(),
),
p.range,
));
}
let size = size.to_u32();
// Step 2: Validate and compile all keys.
let mut seen = Vec::new();
for key in keys {
self.compile_pattern_mapping_key(&mut seen, p.range, key)?;
}
self.set_source_range(p.range);
// Stack: [subject, key1, key2, ..., key_n]
// Build tuple of keys (empty tuple if size==0)
emit!(self, Instruction::BuildTuple { count: size });
// Stack: [subject, keys_tuple]
// Match keys
emit!(self, Instruction::MatchKeys);
// Stack: [subject, keys_tuple, values_or_none]
pc.on_top += 2; // subject and keys_tuple are underneath
// Check if match succeeded
self.set_source_range(p.range);
emit!(self, Instruction::Copy { i: 1 });
// Stack: [subject, keys_tuple, values_tuple, values_tuple_copy]
// Check if copy is None (consumes the copy like POP_JUMP_IF_NONE)
self.set_source_range(p.range);
self.emit_load_const(ConstantData::None);
self.set_source_range(p.range);
emit!(
self,
Instruction::IsOp {
invert: Invert::Yes
}
);
// Stack: [subject, keys_tuple, values_tuple, bool]
self.set_source_range(p.range);
self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse);
// Stack: [subject, keys_tuple, values_tuple]
// Unpack values (the original values_tuple)
emit!(self, Instruction::UnpackSequence { count: size });
// Stack after unpack: [subject, keys_tuple, ...unpacked values...]
if size == 0 {
pc.on_top -= 1;
} else {
pc.on_top += size as usize - 1;
}
// Step 3: Process matched values
for i in 0..size {
pc.on_top -= 1;
self.compile_pattern_subpattern(&patterns[i as usize], pc)?;
}
// After processing subpatterns, adjust on_top
// "Whatever happens next should consume the tuple of keys and the subject"
// Stack currently: [subject, keys_tuple, ...any captured values...]
pc.on_top -= 2;
// Step 4: Handle rest pattern or cleanup
if let Some(rest_name) = star_target {
// Build rest dict for **rest pattern
// Stack: [subject, keys_tuple]
// Build rest dict exactly
self.set_source_range(p.range);
emit!(self, Instruction::BuildMap { count: 0 });
// Stack: [subject, keys_tuple, {}]
self.set_source_range(p.range);
emit!(self, Instruction::Swap { i: 3 });
// Stack: [{}, keys_tuple, subject]
self.set_source_range(p.range);
emit!(self, Instruction::DictUpdate { i: 2 });
// Stack after DICT_UPDATE: [rest_dict, keys_tuple]
// DICT_UPDATE consumes source (subject) and leaves dict in place
// Unpack keys and delete from rest_dict
self.set_source_range(p.range);
emit!(self, Instruction::UnpackSequence { count: size });
// Stack: [rest_dict, k1, k2, ..., kn] (if size==0, nothing pushed)
// Delete each key from rest_dict (skipped when size==0)
// while (size) { COPY(1 + size--); SWAP(2); DELETE_SUBSCR }
let mut remaining = size;
while remaining > 0 {
// Copy rest_dict which is at position (1 + remaining) from TOS
self.set_source_range(p.range);
emit!(self, Instruction::Copy { i: 1 + remaining });
// Stack: [rest_dict, k1, ..., kn, rest_dict]
self.set_source_range(p.range);
emit!(self, Instruction::Swap { i: 2 });
// Stack: [rest_dict, k1, ..., kn-1, rest_dict, kn]
self.set_source_range(p.range);
emit!(self, Instruction::DeleteSubscr);
// Stack: [rest_dict, k1, ..., kn-1] (removed kn from rest_dict)
remaining -= 1;
}
// Stack: [rest_dict] (plus any previously stored values)
// pattern_helper_store_name will handle the rotation correctly
// Store the rest dict
self.pattern_helper_store_name(p.range, Some(rest_name), pc)?;
} else {
// Non-rest pattern: just clean up the stack
// Pop them as we're not using them
self.set_source_range(p.range);
emit!(self, Instruction::PopTop); // Pop keys_tuple
self.set_source_range(p.range);
emit!(self, Instruction::PopTop); // Pop subject
}
Ok(())
}
fn compile_pattern_mapping_key(
&mut self,
seen: &mut Vec<ConstantData>,
pattern_range: TextRange,
key: &ast::Expr,
) -> CompileResult<()> {
let is_attribute = matches!(key, ast::Expr::Attribute(_));
let constant = match self.try_compile_match_mapping_key_constant(key)? {
Some(constant) => Some(constant),
None if is_attribute => None,
None => {
if Self::is_unexpected_match_literal_constant(key) {
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(
"unexpected constant inside of a literal pattern".to_string(),
),
pattern_range,
));
}
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(
"mapping pattern keys may only match literals and attribute lookups"
.to_string(),
),
pattern_range,
));
}
};
if let Some(constant) = constant {
if seen
.iter()
.any(|seen| Self::match_mapping_keys_equal(seen, &constant))
{
let key_repr = Self::match_mapping_key_repr(&constant);
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(format!(
"mapping pattern checks duplicate key ({key_repr})"
)),
pattern_range,
));
}
seen.push(constant);
}
self.compile_match_pattern_expr(key)
}
fn try_compile_match_mapping_key_constant(
&mut self,
key: &ast::Expr,
) -> CompileResult<Option<ConstantData>> {
if let Some(constant) = self.try_fold_match_pattern_const_expr(key)? {
return Ok(Some(constant));
}
self.try_compile_match_mapping_key_direct_constant(key)
}
fn try_compile_match_value_constant(
&mut self,
value: &ast::Expr,
) -> CompileResult<Option<ConstantData>> {
if let Some(constant) = self.try_fold_match_pattern_const_expr(value)? {
return Ok(Some(constant));
}
self.try_compile_match_pattern_direct_literal(value)
}
fn match_mapping_keys_equal(left: &ConstantData, right: &ConstantData) -> bool {
use ConstantData::{Bytes, Ellipsis, None, Str};
if Self::match_mapping_numeric_keys_equal(left, right).unwrap_or(false) {
return true;
}
match (left, right) {
(Str { value: left }, Str { value: right }) => left == right,
(Bytes { value: left }, Bytes { value: right }) => left == right,
(None, None) | (Ellipsis, Ellipsis) => true,
_ => false,
}
}
fn match_mapping_key_repr(key: &ConstantData) -> String {
match key {
ConstantData::Integer { value } => value.to_string(),
ConstantData::Float { value } => literal_float::to_string(*value),
ConstantData::Complex { value } => literal_complex::to_string(value.re, value.im),
ConstantData::Boolean { value } => {
if *value {
"True".to_owned()
} else {
"False".to_owned()
}
}
ConstantData::Str { value } => UnicodeEscape::new_repr(value.as_ref())
.str_repr()
.to_string()
.unwrap_or_else(|| value.to_string()),
ConstantData::Bytes { value } => AsciiEscape::new_repr(value)
.bytes_repr()
.to_string()
.unwrap_or_else(|| format!(r#"b"{}""#, value.escape_ascii())),
ConstantData::None => "None".to_owned(),
ConstantData::Ellipsis => "...".to_owned(),
other => other.to_string(),
}
}
fn match_mapping_numeric_keys_equal(left: &ConstantData, right: &ConstantData) -> Option<bool> {
use ConstantData::{Boolean, Complex, Float, Integer};
match (left, right) {
(Integer { value: left }, Integer { value: right }) => Some(left == right),
(Boolean { value: left }, Boolean { value: right }) => Some(left == right),
(Boolean { value }, Integer { value: int })
| (Integer { value: int }, Boolean { value }) => {
Some(BigInt::from(u8::from(*value)) == *int)
}
(Float { value: left }, Float { value: right }) => Some(left == right),
(Integer { value: int }, Float { value: float })
| (Float { value: float }, Integer { value: int }) => {
Some(Self::match_mapping_float_integer_equal(*float, int))
}
(Boolean { value }, Float { value: float })
| (Float { value: float }, Boolean { value }) => Some(
Self::match_mapping_float_integer_equal(*float, &BigInt::from(u8::from(*value))),
),
(Complex { value: left }, Complex { value: right }) => {
Some(left.re == right.re && left.im == right.im)
}
(Complex { value: complex }, other) | (other, Complex { value: complex }) => Some(
complex.im == 0.0
&& Self::match_mapping_float_real_constant_equal(complex.re, other)
.unwrap_or(false),
),
_ => Option::None,
}
}
fn match_mapping_float_real_constant_equal(float: f64, other: &ConstantData) -> Option<bool> {
match other {
ConstantData::Integer { value } => {
Some(Self::match_mapping_float_integer_equal(float, value))
}
ConstantData::Boolean { value } => Some(Self::match_mapping_float_integer_equal(
float,
&BigInt::from(u8::from(*value)),
)),
ConstantData::Float { value } => Some(float == *value),
_ => None,
}
}
fn match_mapping_float_integer_equal(float: f64, int: &BigInt) -> bool {
Self::match_mapping_float_to_integer(float).is_some_and(|float_int| &float_int == int)
}
fn match_mapping_float_to_integer(value: f64) -> Option<BigInt> {
if !value.is_finite() {
return None;
}
if value == 0.0 {
return Some(BigInt::from(0));
}
let bits = value.to_bits();
let negative = (bits >> 63) != 0;
let exponent_bits = i32::try_from((bits >> 52) & 0x7ff).ok()?;
let fraction = bits & ((1_u64 << 52) - 1);
let (mantissa, exponent) = if exponent_bits == 0 {
(fraction, -1074)
} else {
((1_u64 << 52) | fraction, exponent_bits - 1023 - 52)
};
let mut integer = if exponent >= 0 {
BigInt::from(mantissa) << u32::try_from(exponent).ok()?
} else {
let shift = u32::try_from(-exponent).ok()?;
if shift >= u64::BITS {
return None;
}
let mask = (1_u64 << shift) - 1;
if mantissa & mask != 0 {
return None;
}
BigInt::from(mantissa >> shift)
};
if negative {
integer = -integer;
}
Some(integer)
}
fn compile_pattern_or(
&mut self,
p: &ast::PatternMatchOr,
pc: &mut PatternContext,
) -> CompileResult<()> {
// Ensure the pattern is a MatchOr.
let end = self.new_block(); // Create a new jump target label.
let size = p.patterns.len();
if size <= 1 {
return Err(self.error(CodegenErrorType::SyntaxError(
"MatchOr requires at least 2 patterns".to_owned(),
)));
}
// Save the current pattern context.
let old_pc = pc.clone();
// Simulate Py_INCREF on pc.stores by cloning it.
pc.stores = pc.stores.clone();
let mut control: Option<Vec<Name>> = None; // Will hold the capture list of the first alternative.
// Process each alternative.
for (i, alt) in p.patterns.iter().enumerate() {
// Create a fresh empty store for this alternative.
pc.stores = Vec::new();
// An irrefutable subpattern must be last (if allowed).
pc.allow_irrefutable = (i == size - 1) && old_pc.allow_irrefutable;
// Reset failure targets and the on_top counter.
pc.fail_pop.clear();
pc.on_top = 0;
// Emit a COPY(1) instruction before compiling the alternative.
self.set_source_range(alt.range());
emit!(self, Instruction::Copy { i: 1 });
self.compile_pattern(alt, pc)?;
let n_stores = pc.stores.len();
if i == 0 {
// Save the captured names from the first alternative.
control = Some(pc.stores.clone());
} else {
let control_vec = control.as_ref().unwrap();
if n_stores != control_vec.len() {
return Err(
self.error_ranged(CodegenErrorType::ConflictingNameBindPattern, p.range())
);
} else if n_stores > 0 {
// Check that the names occur in the same order.
for i_control in (0..n_stores).rev() {
let name = &control_vec[i_control];
// Find the index of `name` in the current stores.
let i_stores =
pc.stores.iter().position(|n| n == name).ok_or_else(|| {
self.error_ranged(
CodegenErrorType::ConflictingNameBindPattern,
p.range(),
)
})?;
if i_control != i_stores {
// The orders differ; we must reorder.
assert!(i_stores < i_control, "expected i_stores < i_control");
let rotations = i_stores + 1;
// Rotate pc.stores: take a slice of the first `rotations` items...
let rotated = pc.stores[0..rotations].to_vec();
// Remove those elements.
for _ in 0..rotations {
pc.stores.remove(0);
}
// Insert the rotated slice at the appropriate index.
let insert_pos = i_control - i_stores;
for (j, elem) in rotated.into_iter().enumerate() {
pc.stores.insert(insert_pos + j, elem);
}
// Also perform the same rotation on the evaluation stack.
self.set_source_range(alt.range());
for _ in 0..=i_stores {
self.pattern_helper_rotate(alt.range(), i_control + 1);
}
}
}
}
}
// Emit a jump to the common end label and reset any failure jump targets.
self.set_source_range(alt.range());
emit!(self, PseudoInstruction::Jump { delta: end });
self.set_source_range(alt.range());
self.emit_and_reset_fail_pop(pc, alt.range());
}
// Restore the original pattern context.
*pc = old_pc;
// Simulate Py_INCREF on pc.stores.
pc.stores = pc.stores.clone();
// In C, old_pc.fail_pop is set to NULL to avoid freeing it later.
// In Rust, old_pc is a local clone, so we need not worry about that.
// No alternative matched: pop the subject and fail.
self.set_source_range(p.range());
emit!(self, Instruction::PopTop);
self.jump_to_fail_pop(pc, JumpOp::Jump);
// Use the label "end".
// CPython codegen_pattern_or() emits USE_LABEL(c, end).
self.use_cpython_label_block(end);
// Adjust the final captures.
let n_stores = control.as_ref().unwrap().len();
let n_rots = n_stores + 1 + pc.on_top + pc.stores.len();
for i in 0..n_stores {
// Rotate the capture to its proper place.
self.set_source_range(p.range());
self.pattern_helper_rotate(p.range(), n_rots);
let name = &control.as_ref().unwrap()[i];
// Check for duplicate binding.
if pc.stores.contains(name) {
return Err(self.error_ranged(
CodegenErrorType::DuplicateStore(name.to_string()),
p.range(),
));
}
pc.stores.push(name.clone());
}
// Old context and control will be dropped automatically.
// Finally, pop the copy of the subject.
self.set_source_range(p.range());
emit!(self, Instruction::PopTop);
Ok(())
}
fn compile_pattern_sequence(
&mut self,
p: &ast::PatternMatchSequence,
pc: &mut PatternContext,
) -> CompileResult<()> {
// Ensure the pattern is a MatchSequence.
let patterns = &p.patterns; // a slice of ast::Pattern
let size = patterns.len();
let mut star: Option<usize> = None;
let mut only_wildcard = true;
let mut star_wildcard = false;
// Find a starred pattern, if it exists. There may be at most one.
for (i, pattern) in patterns.iter().enumerate() {
if pattern.is_match_star() {
if star.is_some() {
return Err(self.error_ranged(
CodegenErrorType::MultipleStarredNamesInSequencePattern,
p.range,
));
}
// star wildcard check
star_wildcard = Self::pattern_wildcard_star_check(pattern);
only_wildcard &= star_wildcard;
star = Some(i);
continue;
}
// wildcard check
only_wildcard &= Self::pattern_wildcard_check(pattern);
}
// Keep the subject on top during the sequence and length checks.
pc.on_top += 1;
self.set_source_range(p.range);
emit!(self, Instruction::MatchSequence);
self.set_source_range(p.range);
self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse);
if star.is_none() {
// No star: len(subject) == size
self.set_source_range(p.range);
emit!(self, Instruction::GetLen);
self.set_source_range(p.range);
self.emit_load_const(ConstantData::Integer { value: size.into() });
self.set_source_range(p.range);
emit!(
self,
Instruction::CompareOp {
opname: ComparisonOperator::Equal
}
);
self.set_source_range(p.range);
self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse);
} else if size > 1 {
// Star exists: len(subject) >= size - 1
self.set_source_range(p.range);
emit!(self, Instruction::GetLen);
self.set_source_range(p.range);
self.emit_load_const(ConstantData::Integer {
value: (size - 1).into(),
});
self.set_source_range(p.range);
emit!(
self,
Instruction::CompareOp {
opname: ComparisonOperator::GreaterOrEqual
}
);
self.set_source_range(p.range);
self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse);
}
// Whatever comes next should consume the subject.
pc.on_top -= 1;
if only_wildcard {
// ast::Patterns like: [] / [_] / [_, _] / [*_] / [_, *_] / [_, _, *_] / etc.
self.set_source_range(p.range);
emit!(self, Instruction::PopTop);
} else if star_wildcard {
self.pattern_helper_sequence_subscr(p.range, patterns, star.unwrap(), pc)?;
} else {
self.pattern_helper_sequence_unpack(p.range, patterns, star, pc)?;
}
Ok(())
}
fn compile_pattern_value(
&mut self,
p: &ast::PatternMatchValue,
pc: &mut PatternContext,
) -> CompileResult<()> {
// Match CPython codegen_pattern_value(): compare, then normalize to bool
// before the fail jump. Late IR folding will collapse COMPARE_OP+TO_BOOL
// into COMPARE_OP bool(...) when applicable.
if let Some(constant) = self.try_compile_match_value_constant(&p.value)? {
self.set_source_range(p.value.range());
self.emit_load_const(constant);
} else if matches!(*p.value, ast::Expr::Attribute(_)) {
self.compile_expression(&p.value)?;
} else {
if Self::is_unexpected_match_literal_constant(&p.value) {
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(
"unexpected constant inside of a literal pattern".to_string(),
),
p.range,
));
}
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(
"patterns may only match literals and attribute lookups".to_string(),
),
p.range,
));
}
self.set_source_range(p.range);
emit!(
self,
Instruction::CompareOp {
opname: bytecode::ComparisonOperator::Equal
}
);
self.set_source_range(p.range);
emit!(self, Instruction::ToBool);
self.set_source_range(p.range);
self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse);
Ok(())
}
fn compile_pattern_singleton(
&mut self,
p: &ast::PatternMatchSingleton,
pc: &mut PatternContext,
) {
// Load the singleton constant value.
self.set_source_range(p.range);
self.emit_load_const(match p.value {
ast::Singleton::None => ConstantData::None,
ast::Singleton::False => ConstantData::Boolean { value: false },
ast::Singleton::True => ConstantData::Boolean { value: true },
});
// Compare using the "Is" operator.
self.set_source_range(p.range);
emit!(self, Instruction::IsOp { invert: Invert::No });
// Jump to the failure label if the comparison is false.
self.set_source_range(p.range);
self.jump_to_fail_pop(pc, JumpOp::PopJumpIfFalse);
}
fn compile_pattern(
&mut self,
pattern_type: &ast::Pattern,
pattern_context: &mut PatternContext,
) -> CompileResult<()> {
let prev_source_range = self.current_source_range;
self.set_source_range(pattern_type.range());
let result = match &pattern_type {
ast::Pattern::MatchValue(pattern_type) => {
self.compile_pattern_value(pattern_type, pattern_context)
}
ast::Pattern::MatchSingleton(pattern_type) => {
self.compile_pattern_singleton(pattern_type, pattern_context);
Ok(())
}
ast::Pattern::MatchSequence(pattern_type) => {
self.compile_pattern_sequence(pattern_type, pattern_context)
}
ast::Pattern::MatchMapping(pattern_type) => {
self.compile_pattern_mapping(pattern_type, pattern_context)
}
ast::Pattern::MatchClass(pattern_type) => {
self.compile_pattern_class(pattern_type, pattern_context)
}
ast::Pattern::MatchStar(pattern_type) => {
self.compile_pattern_star(pattern_type, pattern_context)
}
ast::Pattern::MatchAs(pattern_type) => {
self.compile_pattern_as(pattern_type, pattern_context)
}
ast::Pattern::MatchOr(pattern_type) => {
self.compile_pattern_or(pattern_type, pattern_context)
}
};
self.set_source_range(prev_source_range);
result
}
fn compile_match_inner(
&mut self,
subject: &ast::Expr,
cases: &[ast::MatchCase],
pattern_context: &mut PatternContext,
) -> CompileResult<()> {
self.compile_expression(subject)?;
let end = self.new_block();
let num_cases = cases.len();
assert!(num_cases > 0);
let has_default =
num_cases > 1 && Self::pattern_wildcard_check(&cases.last().unwrap().pattern);
let case_count = num_cases - usize::from(has_default);
for (i, m) in cases.iter().enumerate().take(case_count) {
// Only copy the subject if not on the last case
if i != case_count - 1 {
self.set_source_range(m.pattern.range());
emit!(self, Instruction::Copy { i: 1 });
}
pattern_context.stores = Vec::with_capacity(1);
pattern_context.allow_irrefutable = m.guard.is_some() || i == num_cases - 1;
pattern_context.fail_pop.clear();
pattern_context.on_top = 0;
self.compile_pattern(&m.pattern, pattern_context)?;
assert_eq!(pattern_context.on_top, 0);
for name in &pattern_context.stores {
self.set_source_range(m.pattern.range());
self.compile_name(name, NameUsage::Store)?;
}
if let Some(ref guard) = m.guard {
self.ensure_fail_pop(pattern_context, 0);
self.compile_jump_if_inner(
guard,
false,
pattern_context.fail_pop[0],
Some(m.pattern.range()),
)?;
}
if i != case_count - 1 {
if let Some(first_stmt) = m.body.first() {
self.set_source_range(first_stmt.range());
}
emit!(self, Instruction::PopTop);
// CPython emits NEXT_LOCATION here; resolve it after redundant
// NOP removal so a following pass NOP survives.
self.set_last_emitted_lineno_override(ir::NEXT_LOCATION_OVERRIDE);
}
self.compile_statements(&m.body)?;
emit!(self, PseudoInstruction::Jump { delta: end });
self.set_no_location();
self.set_source_range(m.pattern.range());
self.emit_and_reset_fail_pop(pattern_context, m.pattern.range());
}
if has_default {
let m = &cases[num_cases - 1];
self.set_source_range(m.pattern.range());
if num_cases == 1 {
emit!(self, Instruction::PopTop);
} else {
emit!(self, Instruction::Nop);
}
if let Some(ref guard) = m.guard {
self.compile_jump_if_inner(guard, false, end, Some(m.pattern.range()))?;
}
self.compile_statements(&m.body)?;
}
// CPython codegen_match_inner() emits USE_LABEL(c, end).
self.use_cpython_label_block(end);
Ok(())
}
fn compile_match(
&mut self,
subject: &ast::Expr,
cases: &[ast::MatchCase],
) -> CompileResult<()> {
self.enter_conditional_block();
let mut pattern_context = PatternContext::new();
self.compile_match_inner(subject, cases, &mut pattern_context)?;
self.leave_conditional_block();
Ok(())
}
/// [CPython `compiler_addcompare`](https://github.com/python/cpython/blob/627894459a84be3488a1789919679c997056a03c/Python/compile.c#L2880-L2924)
fn compile_addcompare(&mut self, op: ast::CmpOp) {
match op {
ast::CmpOp::Eq => emit!(
self,
Instruction::CompareOp {
opname: ComparisonOperator::Equal
}
),
ast::CmpOp::NotEq => emit!(
self,
Instruction::CompareOp {
opname: ComparisonOperator::NotEqual
}
),
ast::CmpOp::Lt => emit!(
self,
Instruction::CompareOp {
opname: ComparisonOperator::Less
}
),
ast::CmpOp::LtE => emit!(
self,
Instruction::CompareOp {
opname: ComparisonOperator::LessOrEqual
}
),
ast::CmpOp::Gt => emit!(
self,
Instruction::CompareOp {
opname: ComparisonOperator::Greater
}
),
ast::CmpOp::GtE => {
emit!(
self,
Instruction::CompareOp {
opname: ComparisonOperator::GreaterOrEqual
}
)
}
ast::CmpOp::In => emit!(self, Instruction::ContainsOp { invert: Invert::No }),
ast::CmpOp::NotIn => emit!(
self,
Instruction::ContainsOp {
invert: Invert::Yes
}
),
ast::CmpOp::Is => emit!(self, Instruction::IsOp { invert: Invert::No }),
ast::CmpOp::IsNot => emit!(
self,
Instruction::IsOp {
invert: Invert::Yes
}
),
}
}
/// Compile a chained comparison.
///
/// ```py
/// a == b == c == d
/// ```
///
/// Will compile into (pseudo code):
///
/// ```py
/// result = a == b
/// if result:
/// result = b == c
/// if result:
/// result = c == d
/// ```
///
/// # See Also
/// - [CPython `compiler_compare`](https://github.com/python/cpython/blob/627894459a84be3488a1789919679c997056a03c/Python/compile.c#L4678-L4717)
fn compile_compare(
&mut self,
left: &ast::Expr,
ops: &[ast::CmpOp],
comparators: &[ast::Expr],
) -> CompileResult<()> {
// Save the full Compare expression range for COMPARE_OP positions
let compare_range = self.current_source_range;
self.check_compare(compare_range, left, ops, comparators)?;
let (last_op, mid_ops) = ops.split_last().unwrap();
let (last_comparator, mid_comparators) = comparators.split_last().unwrap();
// initialize lhs outside of loop
self.compile_expression(left)?;
if mid_comparators.is_empty() {
self.compile_expression(last_comparator)?;
self.set_source_range(compare_range);
self.compile_addcompare(*last_op);
return Ok(());
}
let cleanup = self.new_block();
// for all comparisons except the last (as the last one doesn't need a conditional jump)
for (op, comparator) in mid_ops.iter().zip(mid_comparators) {
self.compile_expression(comparator)?;
// store rhs for the next comparison in chain
self.set_source_range(compare_range);
emit!(self, Instruction::Swap { i: 2 });
emit!(self, Instruction::Copy { i: 2 });
self.compile_addcompare(*op);
// if comparison result is false, we break with this value; if true, try the next one.
emit!(self, Instruction::Copy { i: 1 });
emit!(self, Instruction::ToBool);
emit!(self, Instruction::PopJumpIfFalse { delta: cleanup });
emit!(self, Instruction::PopTop);
}
self.compile_expression(last_comparator)?;
self.set_source_range(compare_range);
self.compile_addcompare(*last_op);
let end = self.new_block();
emit!(self, PseudoInstruction::JumpNoInterrupt { delta: end });
self.set_no_location();
// early exit left us with stack: `rhs, comparison_result`. We need to clean up rhs.
self.use_cpython_label_block(cleanup);
emit!(self, Instruction::Swap { i: 2 });
emit!(self, Instruction::PopTop);
self.use_cpython_label_block(end);
Ok(())
}
fn compile_jump_if_compare(
&mut self,
left: &ast::Expr,
ops: &[ast::CmpOp],
comparators: &[ast::Expr],
condition: bool,
target_block: BlockIdx,
) -> CompileResult<()> {
let compare_range = self.current_source_range;
self.check_compare(compare_range, left, ops, comparators)?;
let (last_op, mid_ops) = ops.split_last().unwrap();
let (last_comparator, mid_comparators) = comparators.split_last().unwrap();
if mid_comparators.is_empty() {
self.compile_expression(left)?;
self.compile_expression(last_comparator)?;
self.set_source_range(compare_range);
self.compile_addcompare(*last_op);
self.emit_pop_jump_by_condition(condition, target_block);
return Ok(());
}
let cleanup = self.new_block();
self.compile_expression(left)?;
for (op, comparator) in mid_ops.iter().zip(mid_comparators) {
self.compile_expression(comparator)?;
self.set_source_range(compare_range);
emit!(self, Instruction::Swap { i: 2 });
emit!(self, Instruction::Copy { i: 2 });
self.compile_addcompare(*op);
emit!(self, Instruction::ToBool);
emit!(self, Instruction::PopJumpIfFalse { delta: cleanup });
}
self.compile_expression(last_comparator)?;
self.set_source_range(compare_range);
self.compile_addcompare(*last_op);
emit!(self, Instruction::ToBool);
self.emit_pop_jump_by_condition(condition, target_block);
let end = self.new_block();
emit!(self, PseudoInstruction::JumpNoInterrupt { delta: end });
self.set_no_location();
self.use_cpython_label_block(cleanup);
emit!(self, Instruction::PopTop);
if !condition {
emit!(
self,
PseudoInstruction::JumpNoInterrupt {
delta: target_block
}
);
self.set_no_location();
}
self.use_cpython_label_block(end);
Ok(())
}
fn emit_pop_jump_by_condition(&mut self, condition: bool, target_block: BlockIdx) {
if condition {
emit!(
self,
Instruction::PopJumpIfTrue {
delta: target_block
}
);
} else {
emit!(
self,
Instruction::PopJumpIfFalse {
delta: target_block,
}
);
}
}
fn compile_annotation(&mut self, annotation: &ast::Expr) -> CompileResult<()> {
if self.future_annotations {
self.set_source_range(annotation.range());
self.emit_load_const(ConstantData::Str {
value: UnparseExpr::new(annotation, &self.source_file)
.to_string()
.into(),
});
} else {
let was_in_annotation = self.in_annotation;
self.in_annotation = true;
// Special handling for starred annotations (*Ts -> Unpack[Ts])
let result = match annotation {
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
// *args: *Ts (where Ts is a TypeVarTuple).
// Do [annotation_value] = [*Ts].
let saved_range = self.current_source_range;
self.compile_expression(value)?;
self.set_source_range(saved_range);
emit!(self, Instruction::UnpackSequence { count: 1 });
Ok(())
}
_ => self.compile_expression(annotation),
};
self.in_annotation = was_in_annotation;
result?;
}
Ok(())
}
fn compile_check_annotation_expression(&mut self, expression: &ast::Expr) -> CompileResult<()> {
self.compile_expression(expression)?;
self.set_source_range(expression.range());
emit!(self, Instruction::PopTop);
Ok(())
}
fn compile_check_annotation_subscript(&mut self, expression: &ast::Expr) -> CompileResult<()> {
match expression {
ast::Expr::Slice(ast::ExprSlice {
lower, upper, step, ..
}) => {
if let Some(lower) = lower {
self.compile_check_annotation_expression(lower)?;
}
if let Some(upper) = upper {
self.compile_check_annotation_expression(upper)?;
}
if let Some(step) = step {
self.compile_check_annotation_expression(step)?;
}
}
ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
for element in elts {
self.compile_check_annotation_subscript(element)?;
}
}
_ => self.compile_check_annotation_expression(expression)?,
}
Ok(())
}
fn compile_annotated_assign(
&mut self,
target: &ast::Expr,
annotation: &ast::Expr,
value: Option<&ast::Expr>,
simple: bool,
loc: TextRange,
) -> CompileResult<()> {
// Perform the actual assignment first
if let Some(value) = value {
self.compile_expression(value)?;
self.compile_store(target)?;
}
// If we have a simple name in module or class scope, store annotation
if simple
&& !self.ctx.in_func()
&& let ast::Expr::Name(ast::ExprName { id, .. }) = target
{
if self.future_annotations {
// PEP 563: Store stringified annotation directly to __annotations__
// Compile annotation as string
self.compile_annotation(annotation)?;
self.set_source_range(loc);
// Load __annotations__
let annotations_name = self.name("__annotations__");
emit!(
self,
Instruction::LoadName {
namei: annotations_name
}
);
// Load the variable name
self.set_source_range(loc);
self.emit_load_const(ConstantData::Str {
value: self.mangle(id).as_str().into(),
});
// Store: __annotations__[name] = annotation
self.set_source_range(loc);
emit!(self, Instruction::StoreSubscr);
} else {
// PEP 649: Handle conditional annotations
if self.current_symbol_table().has_conditional_annotations {
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) || in_conditional_block;
if is_conditional {
let code_info = self.current_code_info();
let annotation_index = code_info.next_conditional_annotation_index;
code_info.next_conditional_annotation_index += 1;
self.set_source_range(loc);
if matches!(scope_type, CompilerScope::Class) {
let i = self.get_cell_var_index("__conditional_annotations__");
emit!(self, Instruction::LoadDeref { i });
} else {
let namei = self.name("__conditional_annotations__");
emit!(self, Instruction::LoadName { namei });
}
self.emit_load_const(ConstantData::Integer {
value: annotation_index.into(),
});
emit!(self, Instruction::SetAdd { i: 1 });
emit!(self, Instruction::PopTop);
}
}
}
}
if value.is_none() {
match target {
ast::Expr::Attribute(ast::ExprAttribute { value, .. }) => {
self.compile_check_annotation_expression(value)?;
}
ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
self.compile_check_annotation_expression(value)?;
self.compile_check_annotation_subscript(slice)?;
}
_ => {}
}
}
Ok(())
}
fn compile_store(&mut self, target: &ast::Expr) -> CompileResult<()> {
let prev_source_range = self.current_source_range;
self.set_source_range(target.range());
let result = (|| -> CompileResult<()> {
match &target {
ast::Expr::Name(ast::ExprName { id, .. }) => self.store_name(id)?,
ast::Expr::Subscript(ast::ExprSubscript {
value, slice, ctx, ..
}) => {
self.compile_subscript(value, slice, *ctx)?;
}
ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
self.maybe_add_static_attribute_to_class(value, attr.as_str());
self.compile_expression(value)?;
self.set_source_range(self.update_start_location_to_match_attr(
target.range(),
target.range(),
attr.as_str(),
));
let namei = self.name(attr.as_str());
emit!(self, Instruction::StoreAttr { namei });
}
ast::Expr::List(ast::ExprList { elts, .. })
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
let mut seen_star = false;
// Scan for star args:
for (i, element) in elts.iter().enumerate() {
if matches!(element, ast::Expr::Starred(_)) && !seen_star {
let before = i;
let after = elts.len() - i - 1;
if before >= (1 << 8) || after >= ((i32::MAX as usize) >> 8) {
return Err(self.error_ranged(
CodegenErrorType::TooManyStarUnpack,
target.range(),
));
}
let before = before.to_u8().ok_or_else(|| {
self.error_ranged(
CodegenErrorType::TooManyStarUnpack,
target.range(),
)
})?;
let after = after.to_u32();
let counts = bytecode::UnpackExArgs { before, after };
emit!(self, Instruction::UnpackEx { counts });
seen_star = true;
} else if matches!(element, ast::Expr::Starred(_)) {
return Err(self.error(CodegenErrorType::MultipleStarArgs));
}
}
if !seen_star {
emit!(
self,
Instruction::UnpackSequence {
count: elts.len().to_u32(),
}
);
}
for element in elts {
if let ast::Expr::Starred(ast::ExprStarred { value, .. }) = &element {
self.compile_store(value)?;
} else {
self.compile_store(element)?;
}
}
}
_ => {
return Err(self.error(match target {
ast::Expr::Starred(_) => CodegenErrorType::SyntaxError(
"starred assignment target must be in a list or tuple".to_owned(),
),
_ => CodegenErrorType::Assign(target.python_name()),
}));
}
}
Ok(())
})();
self.set_source_range(prev_source_range);
result
}
fn compile_augassign(
&mut self,
target: &ast::Expr,
op: ast::Operator,
value: &ast::Expr,
) -> CompileResult<()> {
let stmt_range = self.current_source_range;
let target_range = target.range();
enum AugAssignKind<'a> {
Name {
id: &'a Name,
},
Subscript {
use_slice_opt: bool,
},
Attr {
idx: bytecode::NameIdx,
attr_range: TextRange,
},
}
let kind = match &target {
ast::Expr::Name(ast::ExprName { id, .. }) => {
self.set_source_range(target_range);
self.compile_name(id, NameUsage::Load)?;
AugAssignKind::Name { id }
}
ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
let use_slice_opt = self.should_apply_two_element_slice_optimization(slice);
self.compile_expression(value)?;
self.set_source_range(target_range);
if use_slice_opt {
let ast::Expr::Slice(slice_expr) = slice.as_ref() else {
unreachable!(
"should_use_slice_optimization should only return true for ast::Expr::Slice"
);
};
self.compile_slice_two_parts(slice_expr)?;
self.set_source_range(target_range);
emit!(self, Instruction::Copy { i: 3 });
emit!(self, Instruction::Copy { i: 3 });
emit!(self, Instruction::Copy { i: 3 });
emit!(self, Instruction::BinarySlice);
} else {
self.compile_expression(slice)?;
self.set_source_range(target_range);
emit!(self, Instruction::Copy { i: 2 });
emit!(self, Instruction::Copy { i: 2 });
emit!(
self,
Instruction::BinaryOp {
op: BinaryOperator::Subscr
}
);
}
AugAssignKind::Subscript { use_slice_opt }
}
ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
let attr = attr.as_str();
self.compile_expression(value)?;
let attr_range =
self.update_start_location_to_match_attr(target_range, target_range, attr);
self.set_source_range(target_range);
emit!(self, Instruction::Copy { i: 1 });
let idx = self.name(attr);
self.set_source_range(attr_range);
self.emit_load_attr(idx);
AugAssignKind::Attr { idx, attr_range }
}
_ => {
return Err(self.error(CodegenErrorType::Assign(target.python_name())));
}
};
self.compile_expression(value)?;
self.set_source_range(stmt_range);
self.compile_op(op, true);
match kind {
AugAssignKind::Name { id } => {
// stack: RESULT
self.set_source_range(target_range);
self.compile_name(id, NameUsage::Store)?;
}
AugAssignKind::Subscript { use_slice_opt } => {
self.set_source_range(target_range);
if use_slice_opt {
// stack: CONTAINER START STOP RESULT
emit!(self, Instruction::Swap { i: 4 });
emit!(self, Instruction::Swap { i: 3 });
emit!(self, Instruction::Swap { i: 2 });
emit!(self, Instruction::StoreSlice);
} else {
// stack: CONTAINER SLICE RESULT
emit!(self, Instruction::Swap { i: 3 });
emit!(self, Instruction::Swap { i: 2 });
emit!(self, Instruction::StoreSubscr);
}
}
AugAssignKind::Attr { idx, attr_range } => {
// stack: CONTAINER RESULT
self.set_source_range(attr_range);
emit!(self, Instruction::Swap { i: 2 });
emit!(self, Instruction::StoreAttr { namei: idx });
}
}
Ok(())
}
fn compile_op(&mut self, op: ast::Operator, inplace: bool) {
let bin_op = match op {
ast::Operator::Add => BinaryOperator::Add,
ast::Operator::Sub => BinaryOperator::Subtract,
ast::Operator::Mult => BinaryOperator::Multiply,
ast::Operator::MatMult => BinaryOperator::MatrixMultiply,
ast::Operator::Div => BinaryOperator::TrueDivide,
ast::Operator::FloorDiv => BinaryOperator::FloorDivide,
ast::Operator::Mod => BinaryOperator::Remainder,
ast::Operator::Pow => BinaryOperator::Power,
ast::Operator::LShift => BinaryOperator::Lshift,
ast::Operator::RShift => BinaryOperator::Rshift,
ast::Operator::BitOr => BinaryOperator::Or,
ast::Operator::BitXor => BinaryOperator::Xor,
ast::Operator::BitAnd => BinaryOperator::And,
};
let op = if inplace { bin_op.as_inplace() } else { bin_op };
emit!(self, Instruction::BinaryOp { op })
}
/// Implement boolean short circuit evaluation logic.
/// https://en.wikipedia.org/wiki/Short-circuit_evaluation
///
/// This means, in a boolean statement 'x and y' the variable y will
/// not be evaluated when x is false.
///
/// The idea is to jump to a label if the expression is either true or false
/// (indicated by the condition parameter).
fn compile_jump_if_inner(
&mut self,
expression: &ast::Expr,
condition: bool,
target_block: BlockIdx,
source_range: Option<TextRange>,
) -> CompileResult<()> {
let prev_source_range = self.current_source_range;
self.set_source_range(source_range.unwrap_or_else(|| expression.range()));
// Compile expression for test, and jump to label if false
let result = match &expression {
ast::Expr::BoolOp(ast::ExprBoolOp { op, values, .. }) => {
let (last_value, prefix_values) = values.split_last().unwrap();
let cond2 = matches!(op, ast::BoolOp::Or);
let next2 = if cond2 != condition {
self.new_block()
} else {
target_block
};
for value in prefix_values {
self.compile_jump_if_inner(value, cond2, next2, source_range)?;
}
self.compile_jump_if_inner(last_value, condition, target_block, source_range)?;
if next2 != target_block {
self.use_cpython_label_block(next2);
}
Ok(())
}
ast::Expr::UnaryOp(ast::ExprUnaryOp {
op: ast::UnaryOp::Not,
operand,
..
}) => self.compile_jump_if_inner(operand, !condition, target_block, source_range),
ast::Expr::If(ast::ExprIf {
test, body, orelse, ..
}) => {
let end = self.new_block();
let next2 = self.new_block();
self.compile_jump_if_inner(test, false, next2, source_range)?;
self.compile_jump_if_inner(body, condition, target_block, source_range)?;
emit!(self, PseudoInstruction::JumpNoInterrupt { delta: end });
self.set_no_location();
self.use_cpython_label_block(next2);
self.compile_jump_if_inner(orelse, condition, target_block, source_range)?;
self.use_cpython_label_block(end);
Ok(())
}
ast::Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
..
}) if ops.len() > 1 => {
self.set_source_range(expression.range());
self.compile_jump_if_compare(left, ops, comparators, condition, target_block)
}
_ => {
// Fall back case which always will work!
self.compile_expression(expression)?;
self.set_source_range(expression.range());
emit!(self, Instruction::ToBool);
if condition {
emit!(
self,
Instruction::PopJumpIfTrue {
delta: target_block,
}
);
} else {
emit!(
self,
Instruction::PopJumpIfFalse {
delta: target_block,
}
);
}
Ok(())
}
};
self.set_source_range(prev_source_range);
result
}
fn compile_jump_if(
&mut self,
expression: &ast::Expr,
condition: bool,
target_block: BlockIdx,
) -> CompileResult<()> {
self.compile_jump_if_inner(expression, condition, target_block, None)
}
/// Compile a boolean operation as an expression.
/// This means, that the last value remains on the stack.
fn compile_bool_op(&mut self, op: ast::BoolOp, values: &[ast::Expr]) -> CompileResult<()> {
let boolop_range = self.current_source_range;
let after_block = self.new_block();
let (last_value, prefix_values) = values.split_last().unwrap();
for value in prefix_values {
self.compile_expression(value)?;
self.set_source_range(boolop_range);
self.emit_short_circuit_test(op, after_block);
self.set_source_range(boolop_range);
emit!(self, Instruction::PopTop);
}
self.compile_expression(last_value)?;
self.use_cpython_label_block(after_block);
Ok(())
}
/// Emit CPython-style pseudo conditional jump for short-circuit evaluation.
/// flowgraph.c lowers it to `COPY 1; TO_BOOL; POP_JUMP_IF_*`.
fn emit_short_circuit_test(&mut self, op: ast::BoolOp, target: BlockIdx) {
match op {
ast::BoolOp::And => {
emit!(self, PseudoInstruction::JumpIfFalse { delta: target });
}
ast::BoolOp::Or => {
emit!(self, PseudoInstruction::JumpIfTrue { delta: target });
}
}
}
fn compile_subdict(
&mut self,
items: &[ast::DictItem],
begin: usize,
end: usize,
range: TextRange,
) -> CompileResult<()> {
let n = end - begin;
let big = n * 2 > STACK_USE_GUIDELINE as usize;
if big {
self.set_source_range(range);
emit!(self, Instruction::BuildMap { count: 0 });
}
for item in &items[begin..end] {
self.compile_expression(item.key.as_ref().unwrap())?;
self.compile_expression(&item.value)?;
if big {
self.set_source_range(range);
emit!(self, Instruction::MapAdd { i: 1 });
}
}
if !big {
self.set_source_range(range);
emit!(self, Instruction::BuildMap { count: n.to_u32() });
}
Ok(())
}
fn compile_dict(&mut self, items: &[ast::DictItem], range: TextRange) -> CompileResult<()> {
let n = items.len();
let mut have_dict = false;
let mut elements = 0usize;
for (i, item) in items.iter().enumerate() {
if item.key.is_none() {
if elements != 0 {
self.compile_subdict(items, i - elements, i, range)?;
if have_dict {
self.set_source_range(range);
emit!(self, Instruction::DictUpdate { i: 1 });
}
have_dict = true;
elements = 0;
}
if !have_dict {
self.set_source_range(range);
emit!(self, Instruction::BuildMap { count: 0 });
have_dict = true;
}
self.compile_expression(&item.value)?;
self.set_source_range(range);
emit!(self, Instruction::DictUpdate { i: 1 });
} else if elements * 2 > STACK_USE_GUIDELINE as usize {
self.compile_subdict(items, i - elements, i + 1, range)?;
if have_dict {
self.set_source_range(range);
emit!(self, Instruction::DictUpdate { i: 1 });
}
have_dict = true;
elements = 0;
} else {
elements += 1;
}
}
if elements != 0 {
self.compile_subdict(items, n - elements, n, range)?;
if have_dict {
self.set_source_range(range);
emit!(self, Instruction::DictUpdate { i: 1 });
}
have_dict = true;
}
if !have_dict {
self.set_source_range(range);
emit!(self, Instruction::BuildMap { count: 0 });
}
Ok(())
}
/// Compile the yield-from/await sequence using SEND/END_SEND/CLEANUP_THROW.
/// compiler_add_yield_from
/// This generates:
/// send:
/// SEND exit
/// SETUP_FINALLY fail (via exception table)
/// YIELD_VALUE 1
/// POP_BLOCK (NO_LOCATION)
/// RESUME
/// JUMP send
/// fail:
/// CLEANUP_THROW
/// JUMP exit
/// exit:
/// END_SEND
fn compile_yield_from_sequence(&mut self, is_await: bool) -> BlockIdx {
let send_block = self.new_block();
let fail_block = self.new_block();
let exit_block = self.new_block();
// send:
self.use_cpython_label_block(send_block);
emit!(self, Instruction::Send { delta: exit_block });
// SETUP_FINALLY fail - set up exception handler for YIELD_VALUE
emit!(self, PseudoInstruction::SetupFinally { delta: fail_block });
// YIELD_VALUE with arg=1 (yield-from/await mode - not wrapped for async gen)
emit!(self, Instruction::YieldValue { arg: 1 });
// POP_BLOCK before RESUME
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
// RESUME
emit!(
self,
Instruction::Resume {
context: if is_await {
oparg::ResumeContext::from(oparg::ResumeLocation::AfterAwait)
} else {
oparg::ResumeContext::from(oparg::ResumeLocation::AfterYieldFrom)
}
}
);
// JUMP_BACKWARD_NO_INTERRUPT send
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: send_block }
);
// fail: CLEANUP_THROW
// Stack when exception: [receiver, yielded_value, exc]
// CLEANUP_THROW: [sub_iter, last_sent_val, exc] -> [None, value]
// CPython lets this block fall through to END_SEND during codegen;
// push_cold_blocks_to_end later inserts the no-interrupt jump after
// moving the cold fail block behind the warm exit path.
self.use_cpython_label_block(fail_block);
emit!(self, Instruction::CleanupThrow);
// exit: END_SEND
// Stack: [receiver, value] (from SEND) or [None, value] (from CLEANUP_THROW)
// END_SEND: [receiver/None, value] -> [value]
self.use_cpython_label_block(exit_block);
emit!(self, Instruction::EndSend);
send_block
}
fn ast_constant_value(&self, expr: &ast::Expr) -> Option<ConstantData> {
expr.as_constant_expr()
.map(|expr| ast_constant_value_to_constant_data(expr.value.clone()))
}
fn single_runtime_interpolation(
expr_tstring: &ast::ExprTString,
) -> Option<(&ast::ConstantValue, Option<&ast::Expr>)> {
let tstring = expr_tstring.as_single_part_tstring()?;
let interpolation = tstring.elements.first()?.as_interpolation()?;
Some((
interpolation.runtime_str.as_ref()?,
interpolation.runtime_interpolation_format_spec.as_deref(),
))
}
fn compile_expression(&mut self, expression: &ast::Expr) -> CompileResult<()> {
trace!("Compiling {expression:?}");
let range = expression.range();
self.set_source_range(range);
if let Some(constant) = self.ast_constant_value(expression) {
self.emit_load_const(constant);
return Ok(());
}
match &expression {
ast::Expr::Call(ast::ExprCall {
func, arguments, ..
}) => self.compile_call(func, arguments)?,
ast::Expr::BoolOp(ast::ExprBoolOp { op, values, .. }) => {
self.compile_bool_op(*op, values)?
}
ast::Expr::BinOp(ast::ExprBinOp {
left, op, right, ..
}) => {
self.compile_expression(left)?;
self.compile_expression(right)?;
// Restore full expression range before emitting the operation
self.set_source_range(range);
self.compile_op(*op, false);
}
ast::Expr::Subscript(ast::ExprSubscript {
value, slice, ctx, ..
}) => {
self.compile_subscript(value, slice, *ctx)?;
}
ast::Expr::UnaryOp(ast::ExprUnaryOp { op, operand, .. }) => {
self.compile_expression(operand)?;
// Restore full expression range before emitting the operation
self.set_source_range(range);
match op {
ast::UnaryOp::UAdd => emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::UnaryPositive
}
),
ast::UnaryOp::USub => emit!(self, Instruction::UnaryNegative),
ast::UnaryOp::Not => {
emit!(self, Instruction::ToBool);
emit!(self, Instruction::UnaryNot);
}
ast::UnaryOp::Invert => emit!(self, Instruction::UnaryInvert),
};
}
ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
// Check for super() attribute access optimization
if let Some(super_type) = self.can_optimize_super_call(value, attr.as_str()) {
// super().attr or super(cls, self).attr optimization
// Stack: [global_super, class, self] → LOAD_SUPER_ATTR → [attr]
let ast::Expr::Call(ast::ExprCall {
func: super_func, ..
}) = value.as_ref()
else {
unreachable!("can_optimize_super_call only accepts calls");
};
self.load_args_for_super(&super_type, super_func.range(), value.range())?;
let attr_access_range =
self.update_start_location_to_match_attr(range, range, attr.as_str());
self.set_source_range(range);
let idx = self.name(attr.as_str());
match super_type {
SuperCallType::TwoArg { .. } => {
self.emit_load_super_attr(idx);
}
SuperCallType::ZeroArg => {
self.emit_load_zero_super_attr(idx);
}
}
self.set_source_range(attr_access_range);
emit!(self, Instruction::Nop);
} else {
// Normal attribute access
self.compile_expression(value)?;
self.set_source_range(self.update_start_location_to_match_attr(
range,
range,
attr.as_str(),
));
let idx = self.name(attr.as_str());
self.emit_load_attr(idx);
}
}
ast::Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
..
}) => {
self.compile_compare(left, ops, comparators)?;
}
ast::Expr::Constant(ast::ExprConstant { value, .. }) => {
self.emit_load_const(ast_constant_value_to_constant_data(value.clone()));
}
ast::Expr::List(ast::ExprList { elts, range, .. }) => {
self.set_source_range(*range);
self.starunpack_helper(elts, 0, CollectionType::List)?;
}
ast::Expr::Tuple(ast::ExprTuple { elts, range, .. }) => {
self.set_source_range(*range);
self.starunpack_helper(elts, 0, CollectionType::Tuple)?;
}
ast::Expr::Set(ast::ExprSet { elts, range, .. }) => {
self.set_source_range(*range);
self.starunpack_helper(elts, 0, CollectionType::Set)?;
}
ast::Expr::Dict(ast::ExprDict { items, range, .. }) => {
self.compile_dict(items, *range)?;
}
ast::Expr::Slice(ast::ExprSlice {
lower,
upper,
step,
range,
..
}) => {
if let Some(folded_const) = self.try_fold_constant_slice(
lower.as_deref(),
upper.as_deref(),
step.as_deref(),
)? {
self.set_source_range(*range);
self.emit_load_const(folded_const);
return Ok(());
}
if let Some(lower) = lower {
self.compile_expression(lower)?;
} else {
self.set_source_range(*range);
self.emit_load_const(ConstantData::None);
}
if let Some(upper) = upper {
self.compile_expression(upper)?;
} else {
self.set_source_range(*range);
self.emit_load_const(ConstantData::None);
}
if let Some(step) = step {
self.compile_expression(step)?;
}
let argc = match step {
Some(_) => BuildSliceArgCount::Three,
None => BuildSliceArgCount::Two,
};
self.set_source_range(*range);
emit!(self, Instruction::BuildSlice { argc });
}
ast::Expr::Yield(ast::ExprYield { value, .. }) => {
if !self.ctx.in_func() {
return Err(self.error(CodegenErrorType::InvalidYield));
}
self.mark_generator();
match value {
Some(expression) => self.compile_expression(expression)?,
Option::None => self.emit_load_const(ConstantData::None),
};
self.set_source_range(range);
if self.ctx.func == FunctionContext::AsyncFunction {
emit!(
self,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::AsyncGenWrap
}
);
}
// arg=0: direct yield (wrapped for async generators)
emit!(self, Instruction::YieldValue { arg: 0 });
emit!(
self,
Instruction::Resume {
context: oparg::ResumeContext::from(oparg::ResumeLocation::AfterYield)
}
);
}
ast::Expr::Await(ast::ExprAwait { value, .. }) => {
if self.ctx.func != FunctionContext::AsyncFunction
&& !self.allows_top_level_await_in_current_context()
{
return Err(self.error(CodegenErrorType::InvalidAwait));
}
self.compile_expression(value)?;
self.set_source_range(range);
emit!(self, Instruction::GetAwaitable { r#where: 0 });
self.emit_load_const(ConstantData::None);
let _ = self.compile_yield_from_sequence(true);
}
ast::Expr::YieldFrom(ast::ExprYieldFrom { value, .. }) => {
match self.ctx.func {
FunctionContext::NoFunction => {
return Err(self.error(CodegenErrorType::InvalidYieldFrom));
}
FunctionContext::AsyncFunction => {
return Err(self.error(CodegenErrorType::AsyncYieldFrom));
}
FunctionContext::Function => {}
}
self.mark_generator();
self.compile_expression(value)?;
self.set_source_range(range);
emit!(self, Instruction::GetYieldFromIter);
self.emit_load_const(ConstantData::None);
let _ = self.compile_yield_from_sequence(false);
}
ast::Expr::Name(ast::ExprName { id, .. }) => self.load_name(id)?,
ast::Expr::Lambda(ast::ExprLambda {
parameters,
body,
range,
..
}) => {
let default_params = ast::Parameters::default();
let params = parameters.as_deref().unwrap_or(&default_params);
validate_duplicate_params(params).map_err(|e| self.error(e))?;
let prev_ctx = self.ctx;
let name = "<lambda>".to_owned();
// Prepare defaults before entering function
let defaults: Vec<_> = core::iter::empty()
.chain(&params.posonlyargs)
.chain(&params.args)
.filter_map(|x| x.default.as_deref())
.collect();
let have_defaults = !defaults.is_empty();
if have_defaults {
let size = defaults.len().to_u32();
for element in &defaults {
self.compile_expression(element)?;
}
self.set_source_range(*range);
emit!(self, Instruction::BuildTuple { count: size });
}
// Prepare keyword-only defaults
let mut kw_with_defaults = vec![];
for kwonlyarg in &params.kwonlyargs {
if let Some(default) = &kwonlyarg.default {
kw_with_defaults.push((&kwonlyarg.parameter, default));
}
}
let have_kwdefaults = !kw_with_defaults.is_empty();
if have_kwdefaults {
let default_kw_count = kw_with_defaults.len();
for (arg, default) in &kw_with_defaults {
self.set_source_range(*range);
self.emit_load_const(ConstantData::Str {
value: self.mangle(arg.name().id()).as_str().into(),
});
self.compile_expression(default)?;
}
self.set_source_range(*range);
emit!(
self,
Instruction::BuildMap {
count: default_kw_count.to_u32(),
}
);
}
self.enter_function(&name, params)?;
let mut func_flags = bytecode::MakeFunctionFlags::new();
if have_defaults {
func_flags.insert(bytecode::MakeFunctionFlag::Defaults);
}
if have_kwdefaults {
func_flags.insert(bytecode::MakeFunctionFlag::KwOnlyDefaults);
}
// Set qualname for lambda
self.set_qualname();
self.ctx = CompileContext {
in_class: prev_ctx.in_class,
func: FunctionContext::Function,
// Lambda is never async, so new scope is not async
in_async_scope: false,
};
self.compile_expression(body)?;
let is_generator = self
.current_code_info()
.flags
.contains(bytecode::CodeFlags::GENERATOR);
if is_generator {
// codegen_lambda() calls OptimizeAndAssemble with
// addNone=0, so AddReturnAtEnd appends RETURN_VALUE without
// adding None to co_consts.
emit!(self, Instruction::ReturnValue);
self.set_no_location();
} else {
self.set_source_range(body.range());
self.emit_return_value();
self.emit_return_const_no_location(ConstantData::None);
}
let code = self.exit_scope();
// Create lambda function with closure
self.set_source_range(*range);
self.make_closure(code, func_flags)?;
self.ctx = prev_ctx;
}
ast::Expr::ListComp(ast::ExprListComp {
elt,
generators,
range,
..
}) => {
self.compile_comprehension(
"<listcomp>",
Some(
Instruction::BuildList {
count: OpArgMarker::marker(),
}
.into(),
),
generators,
&|compiler, collection_add_i| {
compiler.compile_comprehension_element(elt)?;
compiler.set_source_range(elt.range());
emit!(
compiler,
Instruction::ListAppend {
i: collection_add_i.to_u32(),
}
);
Ok(())
},
ComprehensionType::List,
Self::contains_await(elt) || Self::generators_contain_await(generators),
*range,
elt.range(),
elt.range(),
)?;
}
ast::Expr::SetComp(ast::ExprSetComp {
elt,
generators,
range,
..
}) => {
self.compile_comprehension(
"<setcomp>",
Some(
Instruction::BuildSet {
count: OpArgMarker::marker(),
}
.into(),
),
generators,
&|compiler, collection_add_i| {
compiler.compile_comprehension_element(elt)?;
compiler.set_source_range(elt.range());
emit!(
compiler,
Instruction::SetAdd {
i: collection_add_i.to_u32(),
}
);
Ok(())
},
ComprehensionType::Set,
Self::contains_await(elt) || Self::generators_contain_await(generators),
*range,
elt.range(),
elt.range(),
)?;
}
ast::Expr::DictComp(ast::ExprDictComp {
key,
value,
generators,
range,
..
}) => {
let Some(key) = key.as_deref() else {
self.set_source_range(*range);
return Err(self.error(CodegenErrorType::SyntaxError(
"dict unpacking cannot be used in dict comprehension".to_owned(),
)));
};
self.compile_comprehension(
"<dictcomp>",
Some(
Instruction::BuildMap {
count: OpArgMarker::marker(),
}
.into(),
),
generators,
&|compiler, collection_add_i| {
// changed evaluation order for Py38 named expression PEP 572
compiler.compile_expression(key)?;
compiler.compile_expression(value)?;
compiler.set_source_range(TextRange::new(
key.range().start(),
value.range().end(),
));
emit!(
compiler,
Instruction::MapAdd {
i: collection_add_i.to_u32(),
}
);
Ok(())
},
ComprehensionType::Dict,
Self::contains_await(key)
|| Self::contains_await(value)
|| Self::generators_contain_await(generators),
*range,
TextRange::new(key.range().start(), value.range().end()),
key.range(),
)?;
}
ast::Expr::Generator(ast::ExprGenerator {
elt,
generators,
range,
..
}) => {
self.compile_generator_expression(elt, generators, *range)?;
}
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
if self.in_annotation {
// In annotation context, starred expressions are allowed (PEP 646)
// For now, just compile the inner value without wrapping with Unpack
// This is a temporary solution until we figure out how to properly import typing
self.compile_expression(value)?;
} else {
return Err(self.error(CodegenErrorType::InvalidStarExpr));
}
}
ast::Expr::If(ast::ExprIf {
test, body, orelse, ..
}) => {
let after_block = self.new_block();
let else_block = self.new_block();
self.compile_jump_if(test, false, else_block)?;
// True case
self.compile_expression(body)?;
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: after_block }
);
self.set_no_location();
// False case
self.use_cpython_label_block(else_block);
self.compile_expression(orelse)?;
// End
self.use_cpython_label_block(after_block);
}
ast::Expr::Named(ast::ExprNamed {
target,
value,
node_index: _,
range,
}) => {
// Walrus targets in inlined comps should NOT be hidden from locals()
if self.current_code_info().in_inlined_comp
&& let ast::Expr::Name(ast::ExprName { id, .. }) = target.as_ref()
{
let name = self.mangle(id);
let info = self.code_stack.last_mut().unwrap();
info.metadata.fast_hidden.insert(name.to_string(), false);
info.metadata
.fast_hidden_final
.swap_remove(name.into_owned().as_str());
}
self.compile_expression(value)?;
self.set_source_range(*range);
emit!(self, Instruction::Copy { i: 1 });
self.compile_store(target)?;
self.set_source_range(target.range());
}
ast::Expr::FString(fstring) => {
if let Some(joined_str) = fstring.runtime_joined_str.as_ref() {
return self.compile_runtime_joined_str(fstring, joined_str);
}
self.compile_expr_fstring(fstring)?;
}
ast::Expr::TString(tstring) => {
if let Some(template_str) = tstring.runtime_template_str.as_ref() {
return self.compile_runtime_template_str(tstring, template_str);
}
if let Some(interpolation) = Self::single_runtime_interpolation(tstring)
&& self.compile_runtime_interpolation(tstring, interpolation)?
{
return Ok(());
}
self.compile_expr_tstring(tstring)?;
}
ast::Expr::StringLiteral(string) => {
let value = string_literal_value(&self.source_file, &string.value);
self.emit_load_const(ConstantData::Str { value });
}
ast::Expr::BytesLiteral(bytes) => {
let iter = bytes.value.iter().flat_map(|x| x.iter().copied());
let v: Vec<u8> = iter.collect();
self.emit_load_const(ConstantData::Bytes { value: v });
}
ast::Expr::NumberLiteral(number) => match &number.value {
ast::Number::Int(int) => {
let value = ruff_int_to_bigint(int).map_err(|e| self.error(e))?;
self.emit_load_const(ConstantData::Integer { value });
}
ast::Number::Float(float) => {
self.emit_load_const(ConstantData::Float { value: *float });
}
ast::Number::Complex { real, imag } => {
self.emit_load_const(ConstantData::Complex {
value: Complex::new(*real, *imag),
});
}
},
ast::Expr::BooleanLiteral(b) => {
self.emit_load_const(ConstantData::Boolean { value: b.value });
}
ast::Expr::NoneLiteral(_) => {
self.emit_load_const(ConstantData::None);
}
ast::Expr::EllipsisLiteral(_) => {
self.emit_load_const(ConstantData::Ellipsis);
}
ast::Expr::IpyEscapeCommand(expr) => {
return Err(self.error_ranged(
CodegenErrorType::SyntaxError("invalid syntax".to_owned()),
expr.range,
));
}
}
Ok(())
}
/// The called name of a `name(genexpr)` call, the shape
/// `maybe_optimize_function_call()` reserves a `skip_optimization` label
/// for. An `await` or an `async for` inside the generator does not
/// disqualify it: the inlined loop raises the same `TypeError` that
/// calling the builtin on an async generator would.
fn cpython_genexpr_call_name<'a>(
&self,
func: &'a ast::Expr,
args: &ast::Arguments,
) -> Option<&'a str> {
let ast::Expr::Name(ast::ExprName { id, .. }) = func else {
return None;
};
let [ast::Expr::Generator(ast::ExprGenerator { .. })] = &args.args[..] else {
return None;
};
if !args.keywords.is_empty() {
return None;
}
let table = self.current_symbol_table();
table.sub_tables.get(table.next_sub_table)?;
Some(id.as_str())
}
fn detect_builtin_generator_call(
&self,
func: &ast::Expr,
args: &ast::Arguments,
) -> Option<BuiltinGeneratorCallKind> {
match self.cpython_genexpr_call_name(func, args)? {
"tuple" => Some(BuiltinGeneratorCallKind::Tuple),
"all" => Some(BuiltinGeneratorCallKind::All),
"any" => Some(BuiltinGeneratorCallKind::Any),
_ => None,
}
}
/// Emit the optimized inline loop for builtin(genexpr) calls.
///
/// Stack on entry: `[func]` where `func` is the builtin candidate.
/// On return the compiler is positioned at the skip-optimization block so the
/// normal call path can compile the original generator argument again.
fn optimize_builtin_generator_call(
&mut self,
kind: BuiltinGeneratorCallKind,
generator_expr: &ast::Expr,
loc: TextRange,
end: BlockIdx,
) -> CompileResult<()> {
let common_constant = match kind {
BuiltinGeneratorCallKind::Tuple => bytecode::CommonConstant::BuiltinTuple,
BuiltinGeneratorCallKind::All => bytecode::CommonConstant::BuiltinAll,
BuiltinGeneratorCallKind::Any => bytecode::CommonConstant::BuiltinAny,
};
let skip_optimization = self.new_block();
// Stack: [func] - copy function for identity check
self.set_source_range(loc);
emit!(self, Instruction::Copy { i: 1 });
emit!(
self,
Instruction::LoadCommonConstant {
idx: common_constant
}
);
emit!(self, Instruction::IsOp { invert: Invert::No });
emit!(
self,
Instruction::PopJumpIfFalse {
delta: skip_optimization
}
);
emit!(self, Instruction::PopTop);
if matches!(kind, BuiltinGeneratorCallKind::Tuple) {
self.set_source_range(loc);
emit!(self, Instruction::BuildList { count: 0 });
}
let symbol_table_cursors = self.current_symbol_table_cursors();
self.compile_expression(generator_expr)?;
self.set_symbol_table_cursors(symbol_table_cursors);
let loop_block = self.new_block();
let cleanup = self.new_block();
self.use_cpython_label_block(loop_block);
self.set_source_range(loc);
emit!(self, Instruction::ForIter { delta: cleanup });
match kind {
BuiltinGeneratorCallKind::Tuple => {
self.set_source_range(loc);
emit!(self, Instruction::ListAppend { i: 2 });
self.set_source_range(loc);
emit!(self, PseudoInstruction::Jump { delta: loop_block });
}
BuiltinGeneratorCallKind::All => {
self.set_source_range(loc);
emit!(self, Instruction::ToBool);
emit!(self, Instruction::PopJumpIfTrue { delta: loop_block });
emit!(self, Instruction::PopIter);
self.set_no_location();
self.set_source_range(loc);
self.emit_load_const(ConstantData::Boolean { value: false });
self.set_source_range(loc);
emit!(self, PseudoInstruction::Jump { delta: end });
}
BuiltinGeneratorCallKind::Any => {
self.set_source_range(loc);
emit!(self, Instruction::ToBool);
emit!(self, Instruction::PopJumpIfFalse { delta: loop_block });
emit!(self, Instruction::PopIter);
self.set_no_location();
self.set_source_range(loc);
self.emit_load_const(ConstantData::Boolean { value: true });
self.set_source_range(loc);
emit!(self, PseudoInstruction::Jump { delta: end });
}
}
self.use_cpython_label_block(cleanup);
emit!(self, Instruction::EndFor);
self.set_no_location();
emit!(self, Instruction::PopIter);
self.set_no_location();
match kind {
BuiltinGeneratorCallKind::Tuple => {
self.set_source_range(loc);
emit!(
self,
Instruction::CallIntrinsic1 {
func: IntrinsicFunction1::ListToTuple
}
);
}
BuiltinGeneratorCallKind::All => {
self.set_source_range(loc);
self.emit_load_const(ConstantData::Boolean { value: true });
}
BuiltinGeneratorCallKind::Any => {
self.set_source_range(loc);
self.emit_load_const(ConstantData::Boolean { value: false });
}
}
self.set_source_range(loc);
emit!(self, PseudoInstruction::Jump { delta: end });
self.use_cpython_label_block(skip_optimization);
Ok(())
}
fn can_use_cpython_method_call(&self, value: &ast::Expr, args: &ast::Arguments) -> bool {
let is_import = matches!(value, ast::Expr::Name(ast::ExprName { id, .. })
if self.is_name_imported(id.as_str()));
if is_import {
return false;
}
if args.args.len() + args.keywords.len() + usize::from(!args.keywords.is_empty())
>= STACK_USE_GUIDELINE as usize
{
return false;
}
!args
.args
.iter()
.any(|arg| matches!(arg, ast::Expr::Starred(_)))
&& args.keywords.iter().all(|kw| kw.arg.is_some())
}
fn compile_method_call_arguments(
&mut self,
args: &ast::Arguments,
call_range: TextRange,
kw_names_range: TextRange,
) -> CompileResult<()> {
for arg in &args.args {
self.compile_expression(arg)?;
}
if args.keywords.is_empty() {
self.set_source_range(call_range);
emit!(
self,
Instruction::Call {
argc: args.args.len().to_u32()
}
);
return Ok(());
}
let mut kwarg_names = Vec::with_capacity(args.keywords.len());
for keyword in &args.keywords {
kwarg_names.push(ConstantData::Str {
value: keyword.arg.as_ref().unwrap().as_str().into(),
});
self.compile_expression(&keyword.value)?;
}
self.set_source_range(kw_names_range);
self.emit_load_const(ConstantData::Tuple {
elements: kwarg_names,
});
self.set_source_range(call_range);
emit!(
self,
Instruction::CallKw {
argc: (args.args.len() + args.keywords.len()).to_u32()
}
);
Ok(())
}
fn compile_call(&mut self, func: &ast::Expr, args: &ast::Arguments) -> CompileResult<()> {
// Save the call expression's source range so CALL instructions use the
// call start line, not the last argument's line.
let call_range = self.current_source_range;
self.validate_keywords(&args.keywords)?;
let uses_ex_call = self.call_uses_ex_call(args);
// Method call: obj → LOAD_ATTR_METHOD → [method, self_or_null] → args → CALL
// Regular call: func → PUSH_NULL → args → CALL
if let ast::Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = &func {
if !self.can_use_cpython_method_call(value, args) {
self.check_caller(func)?;
self.compile_expression(func)?;
self.set_source_range(func.range());
emit!(self, Instruction::PushNull);
self.codegen_call_helper(0, args, call_range, None)?;
return Ok(());
}
// Check for super() method call optimization
if let Some(super_type) = self.can_optimize_super_call(value, attr.as_str()) {
// super().method() or super(cls, self).method() optimization
// CALL path: [global_super, class, self] → LOAD_SUPER_METHOD → [method, self]
let ast::Expr::Call(ast::ExprCall {
func: super_func, ..
}) = value.as_ref()
else {
unreachable!("can_optimize_super_call only accepts calls");
};
self.load_args_for_super(&super_type, super_func.range(), value.range())?;
let attr_access_range = self.update_start_location_to_match_attr(
func.range(),
func.range(),
attr.as_str(),
);
let method_call_range = self.update_start_location_to_match_attr(
call_range,
func.range(),
attr.as_str(),
);
self.set_source_range(func.range());
let idx = self.name(attr.as_str());
match super_type {
SuperCallType::TwoArg { .. } => {
self.emit_load_super_method(idx);
}
SuperCallType::ZeroArg => {
self.emit_load_zero_super_method(idx);
}
}
// NOP for line tracking at .method( line
self.set_source_range(attr_access_range);
emit!(self, Instruction::Nop);
// CALL at .method( line (not the full expression line)
self.compile_method_call_arguments(args, method_call_range, attr_access_range)?;
} else {
self.compile_expression(value)?;
let idx = self.name(attr.as_str());
let attr_access_range = self.update_start_location_to_match_attr(
func.range(),
func.range(),
attr.as_str(),
);
let method_call_range = self.update_start_location_to_match_attr(
call_range,
func.range(),
attr.as_str(),
);
self.set_source_range(attr_access_range);
self.emit_load_attr_method(idx);
self.compile_method_call_arguments(args, method_call_range, attr_access_range)?;
}
} else if let Some(kind) = (!uses_ex_call)
.then(|| self.detect_builtin_generator_call(func, args))
.flatten()
{
let skip_normal_call = self.new_block();
self.check_caller(func)?;
self.compile_expression(func)?;
self.optimize_builtin_generator_call(
kind,
&args.args[0],
func.range(),
skip_normal_call,
)?;
self.set_source_range(func.range());
emit!(self, Instruction::PushNull);
self.codegen_call_helper(0, args, call_range, None)?;
self.use_cpython_label_block(skip_normal_call);
} else {
// Regular call: push func, then NULL for self_or_null slot
// Stack layout: [func, NULL, args...] - same as method call [func, self, args...]
// CPython `codegen_call()` always creates and uses
// `skip_normal_call`, even when `maybe_optimize_function_call()`
// leaves it untargeted.
let skip_normal_call = self.current_code_info().new_instr_sequence_label();
let genexpr_call_name = (!uses_ex_call)
.then(|| self.cpython_genexpr_call_name(func, args))
.flatten()
.is_some();
self.check_caller(func)?;
self.compile_expression(func)?;
if genexpr_call_name {
// CPython `maybe_optimize_function_call()` creates and uses
// `skip_optimization` for every name(genexpr) shape after
// loading the function, even when the name is not all/any/tuple.
let skip_optimization = self.current_code_info().new_instr_sequence_label();
let result = self
.current_code_info()
.use_raw_instr_sequence_label(skip_optimization);
unwrap_internal(self, result);
}
self.set_source_range(func.range());
emit!(self, Instruction::PushNull);
self.codegen_call_helper(0, args, call_range, None)?;
let result = self
.current_code_info()
.use_raw_instr_sequence_label(skip_normal_call);
unwrap_internal(self, result);
}
Ok(())
}
fn call_uses_ex_call(&self, arguments: &ast::Arguments) -> bool {
let has_starred = arguments
.args
.iter()
.any(|arg| matches!(arg, ast::Expr::Starred(_)));
let has_double_star = arguments.keywords.iter().any(|k| k.arg.is_none());
let too_big =
arguments.args.len() + arguments.keywords.len() * 2 > STACK_USE_GUIDELINE as usize;
has_starred || has_double_star || too_big
}
/// Reject duplicate keyword-argument names in a call.
fn validate_keywords(&mut self, keywords: &[ast::Keyword]) -> CompileResult<()> {
for (i, keyword) in keywords.iter().enumerate() {
let Some(arg) = &keyword.arg else {
continue;
};
for other in &keywords[i + 1..] {
if other
.arg
.as_ref()
.is_some_and(|other| other.as_str() == arg.as_str())
{
return Err(self.error_ranged(
CodegenErrorType::SyntaxError(format!("keyword argument repeated: {arg}")),
other.range,
));
}
}
}
Ok(())
}
/// Compile subkwargs: emit key-value pairs for BUILD_MAP
fn codegen_subkwargs(
&mut self,
keywords: &[ast::Keyword],
begin: usize,
end: usize,
call_range: TextRange,
) -> CompileResult<()> {
let n = end - begin;
assert!(n > 0);
// For large kwargs, use BUILD_MAP(0) + MAP_ADD to avoid stack overflow.
let big = n * 2 > STACK_USE_GUIDELINE as usize;
if big {
emit!(self, Instruction::BuildMap { count: 0 });
self.set_no_location();
}
for kw in &keywords[begin..end] {
// Key first, then value - this is critical!
self.set_source_range(call_range);
self.emit_load_const(ConstantData::Str {
value: kw.arg.as_ref().unwrap().as_str().into(),
});
self.compile_expression(&kw.value)?;
if big {
emit!(self, Instruction::MapAdd { i: 1 });
self.set_no_location();
}
}
if !big {
self.set_source_range(call_range);
emit!(self, Instruction::BuildMap { count: n.to_u32() });
}
Ok(())
}
/// Compile call arguments and emit the appropriate CALL instruction.
/// `call_range` is the source range of the call expression, used to set
/// the correct line number on the CALL instruction.
fn codegen_call_helper(
&mut self,
additional_positional: u32,
arguments: &ast::Arguments,
call_range: TextRange,
kw_names_range: Option<TextRange>,
) -> CompileResult<()> {
self.codegen_call_helper_impl(
additional_positional,
&arguments.args,
&arguments.keywords,
call_range,
kw_names_range,
None,
)
}
fn codegen_call_helper_impl(
&mut self,
additional_positional: u32,
args: &[ast::Expr],
keywords: &[ast::Keyword],
call_range: TextRange,
kw_names_range: Option<TextRange>,
injected_arg: Option<&Name>,
) -> CompileResult<()> {
self.validate_keywords(keywords)?;
let nelts = args.len();
let nkwelts = keywords.len();
// Check if we have starred args or **kwargs
let has_starred = args.iter().any(|arg| matches!(arg, ast::Expr::Starred(_)));
let has_double_star = keywords.iter().any(|k| k.arg.is_none());
// Check if exceeds CPython's stack-use guideline.
// With CALL_KW, kwargs values go on stack but keys go in a const tuple,
// so stack usage is: func + null + positional_args + kwarg_values + kwnames_tuple
let too_big = nelts + nkwelts * 2 > STACK_USE_GUIDELINE as usize;
if !has_starred && !has_double_star && !too_big {
// Simple call path: no * or ** args
for arg in args {
self.compile_expression(arg)?;
}
let injected_count = if let Some(injected_arg) = injected_arg {
self.set_source_range(call_range);
self.load_name(injected_arg)?;
1
} else {
0
};
if nkwelts > 0 {
// Compile keyword values and build kwnames tuple
let mut kwarg_names = Vec::with_capacity(nkwelts);
for keyword in keywords {
kwarg_names.push(ConstantData::Str {
value: keyword.arg.as_ref().unwrap().as_str().into(),
});
self.compile_expression(&keyword.value)?;
}
// Restore call expression range for kwnames and CALL_KW
self.set_source_range(kw_names_range.unwrap_or(call_range));
self.emit_load_const(ConstantData::Tuple {
elements: kwarg_names,
});
self.set_source_range(call_range);
let argc =
additional_positional + nelts.to_u32() + injected_count + nkwelts.to_u32();
emit!(self, Instruction::CallKw { argc });
} else {
self.set_source_range(call_range);
let argc = additional_positional + nelts.to_u32() + injected_count;
emit!(self, Instruction::Call { argc });
}
} else {
// ex_call path: has * or ** args
// Compile positional arguments
if additional_positional == 0 && nelts == 1 && matches!(args[0], ast::Expr::Starred(_))
{
// Single starred arg: pass value directly to CallFunctionEx.
// Runtime will convert to tuple and validate with function name.
if let ast::Expr::Starred(ast::ExprStarred { value, .. }) = &args[0] {
self.compile_expression(value)?;
}
} else {
// CPython `codegen_call_helper_impl()` sends every other
// CALL_FUNCTION_EX positional shape through
// `starunpack_helper_impl(..., BUILD_LIST, LIST_APPEND,
// LIST_EXTEND, tuple=1)`, even when the only reason for the
// ex-call path is too many non-starred positional arguments.
self.set_source_range(call_range);
self.starunpack_helper_impl(
args,
injected_arg,
additional_positional,
CollectionType::Tuple,
)?;
}
self.compile_call_function_ex_keywords(keywords, call_range)?;
self.set_source_range(call_range);
emit!(self, Instruction::CallFunctionEx);
}
Ok(())
}
fn compile_call_function_ex_keywords(
&mut self,
keywords: &[ast::Keyword],
call_range: TextRange,
) -> CompileResult<()> {
if keywords.is_empty() {
self.set_source_range(call_range);
emit!(self, Instruction::PushNull);
return Ok(());
}
let mut have_dict = false;
let mut nseen = 0usize;
for (i, keyword) in keywords.iter().enumerate() {
if keyword.arg.is_none() {
if nseen > 0 {
self.codegen_subkwargs(keywords, i - nseen, i, call_range)?;
if have_dict {
self.set_source_range(call_range);
emit!(self, Instruction::DictMerge { i: 1 });
}
have_dict = true;
nseen = 0;
}
if !have_dict {
self.set_source_range(call_range);
emit!(self, Instruction::BuildMap { count: 0 });
have_dict = true;
}
self.compile_expression(&keyword.value)?;
self.set_source_range(call_range);
emit!(self, Instruction::DictMerge { i: 1 });
} else {
nseen += 1;
}
}
if nseen > 0 {
self.codegen_subkwargs(keywords, keywords.len() - nseen, keywords.len(), call_range)?;
if have_dict {
self.set_source_range(call_range);
emit!(self, Instruction::DictMerge { i: 1 });
}
have_dict = true;
}
debug_assert!(have_dict);
Ok(())
}
fn compile_comprehension_element(&mut self, element: &ast::Expr) -> CompileResult<()> {
self.compile_expression(element).map_err(|e| {
if let CodegenErrorType::InvalidStarExpr = e.error {
self.error(CodegenErrorType::SyntaxError(
"iterable unpacking cannot be used in comprehension".to_owned(),
))
} else {
e
}
})
}
fn compile_generator_expression(
&mut self,
elt: &ast::Expr,
generators: &[ast::Comprehension],
range: TextRange,
) -> CompileResult<()> {
// Check if element or generators contain async content
// This makes the generator expression into an async generator
let element_contains_await =
Self::contains_await(elt) || Self::generators_contain_await(generators);
self.compile_comprehension(
"<genexpr>",
None,
generators,
&|compiler, _collection_add_i| {
// Compile the element expression
// Note: if element is an async comprehension, compile_expression
// already handles awaiting it, so we don't need to await again here
compiler.compile_comprehension_element(elt)?;
compiler.mark_generator();
if compiler.ctx.func == FunctionContext::AsyncFunction {
compiler.set_source_range(elt.range());
emit!(
compiler,
Instruction::CallIntrinsic1 {
func: bytecode::IntrinsicFunction1::AsyncGenWrap
}
);
}
// arg=0: direct yield (wrapped for async generators)
compiler.set_source_range(elt.range());
emit!(compiler, Instruction::YieldValue { arg: 0 });
emit!(
compiler,
Instruction::Resume {
context: oparg::ResumeContext::from(oparg::ResumeLocation::AfterYield)
}
);
emit!(compiler, Instruction::PopTop);
Ok(())
},
ComprehensionType::Generator,
element_contains_await,
range,
elt.range(),
elt.range(),
)
}
fn consume_next_sub_table(&mut self) -> CompileResult<()> {
{
let _ = self.push_symbol_table()?;
}
self.pop_symbol_table();
Ok(())
}
fn consume_function_annotation_symbol_table_if_used(&mut self) -> CompileResult<()> {
if !self.next_function_annotation_symbol_table_uses_annotations() {
return Ok(());
}
if !self.push_annotation_symbol_table() {
let current_table = self.current_symbol_table();
return Err(self.error(CodegenErrorType::SyntaxError(format!(
"no annotation symbol table available in {} (type: {:?})",
current_table.name, current_table.typ
))));
}
self.pop_annotation_symbol_table();
Ok(())
}
fn consume_skipped_nested_scopes_in_expr(
&mut self,
expression: &ast::Expr,
) -> CompileResult<()> {
use ast::visitor::Visitor;
struct SkippedScopeVisitor<'a, 'warnings> {
compiler: &'a mut Compiler<'warnings>,
error: Option<CodegenError>,
}
impl SkippedScopeVisitor<'_, '_> {
fn consume_scope(&mut self) {
if self.error.is_none() {
self.error = self.compiler.consume_next_sub_table().err();
}
}
fn consume_inlined_comprehension_scope(&mut self) -> bool {
if self.error.is_some() {
return false;
}
let Some(current_table) = self.compiler.symbol_table_stack.last_mut() else {
return false;
};
if current_table.next_inlined_comprehension_block
< current_table.inlined_comprehension_blocks.len()
{
current_table.next_inlined_comprehension_block += 1;
true
} else {
false
}
}
fn visit_comprehension_tail(
&mut self,
elt1: &ast::Expr,
elt2: Option<&ast::Expr>,
generators: &[ast::Comprehension],
) {
if let Some(outermost) = generators.first() {
self.visit_expr(&outermost.target);
for if_expr in &outermost.ifs {
self.visit_expr(if_expr);
}
}
for generator in generators.iter().skip(1) {
self.visit_expr(&generator.target);
self.visit_expr(&generator.iter);
for if_expr in &generator.ifs {
self.visit_expr(if_expr);
}
}
if let Some(elt2) = elt2 {
self.visit_expr(elt2);
}
self.visit_expr(elt1);
}
}
impl ast::visitor::Visitor<'_> for SkippedScopeVisitor<'_, '_> {
fn visit_expr(&mut self, expr: &ast::Expr) {
if self.error.is_some() {
return;
}
match expr {
ast::Expr::Lambda(ast::ExprLambda { parameters, .. }) => {
// Defaults are scanned before enter_scope in the
// symbol table builder, so their nested scopes
// precede the lambda scope in sub_tables.
if let Some(params) = parameters.as_deref() {
for default in params
.posonlyargs
.iter()
.chain(&params.args)
.chain(&params.kwonlyargs)
.filter_map(|p| p.default.as_deref())
{
self.visit_expr(default);
}
}
self.consume_scope();
}
ast::Expr::Generator(ast::ExprGenerator { generators, .. }) => {
if let Some(first) = generators.first() {
self.visit_expr(&first.iter);
}
self.consume_scope();
}
ast::Expr::ListComp(ast::ExprListComp {
elt, generators, ..
})
| ast::Expr::SetComp(ast::ExprSetComp {
elt, generators, ..
}) => {
if let Some(first) = generators.first() {
self.visit_expr(&first.iter);
}
if self.consume_inlined_comprehension_scope() {
self.visit_comprehension_tail(elt, None, generators);
} else {
self.consume_scope();
}
}
ast::Expr::DictComp(ast::ExprDictComp {
key,
value,
generators,
..
}) => {
if let Some(first) = generators.first() {
self.visit_expr(&first.iter);
}
if self.consume_inlined_comprehension_scope() {
if let Some(key) = key.as_deref() {
self.visit_comprehension_tail(key, Some(value), generators);
} else {
self.visit_comprehension_tail(value, None, generators);
}
} else {
self.consume_scope();
}
}
_ => ast::visitor::walk_expr(self, expr),
}
}
}
let mut visitor = SkippedScopeVisitor {
compiler: self,
error: None,
};
visitor.visit_expr(expression);
if let Some(err) = visitor.error {
Err(err)
} else {
Ok(())
}
}
fn consume_skipped_nested_scopes_in_parameter_defaults(
&mut self,
parameters: &ast::Parameters,
) -> CompileResult<()> {
for default in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.filter_map(|arg| arg.default.as_deref())
{
self.consume_skipped_nested_scopes_in_expr(default)?;
}
Ok(())
}
fn consume_skipped_nested_scopes_in_statements(
&mut self,
statements: &[ast::Stmt],
) -> CompileResult<()> {
use ast::visitor::Visitor;
struct SkippedStatementScopeVisitor<'a, 'warnings> {
compiler: &'a mut Compiler<'warnings>,
error: Option<CodegenError>,
}
impl SkippedStatementScopeVisitor<'_, '_> {
fn consume_scope(&mut self) {
if self.error.is_none() {
self.error = self.compiler.consume_next_sub_table().err();
}
}
fn consume_function_annotation_scope_if_used(&mut self) {
if self.error.is_none() {
self.error = self
.compiler
.consume_function_annotation_symbol_table_if_used()
.err();
}
}
fn visit_parameter_defaults(&mut self, parameters: &ast::Parameters) {
for default in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.filter_map(|arg| arg.default.as_deref())
{
self.visit_expr(default);
}
}
fn visit_decorators(&mut self, decorators: &[ast::Decorator]) {
for decorator in decorators {
self.visit_expr(&decorator.expression);
}
}
fn visit_arguments(&mut self, arguments: &ast::Arguments) {
for arg in &arguments.args {
self.visit_expr(arg);
}
for keyword in &arguments.keywords {
self.visit_expr(&keyword.value);
}
}
}
impl ast::visitor::Visitor<'_> for SkippedStatementScopeVisitor<'_, '_> {
fn visit_stmt(&mut self, stmt: &ast::Stmt) {
if self.error.is_some() {
return;
}
match stmt {
ast::Stmt::FunctionDef(ast::StmtFunctionDef {
parameters,
decorator_list,
type_params,
..
}) => {
self.visit_parameter_defaults(parameters);
self.visit_decorators(decorator_list);
if type_params.is_some() {
self.consume_scope();
} else {
self.consume_function_annotation_scope_if_used();
self.consume_scope();
}
}
ast::Stmt::ClassDef(ast::StmtClassDef {
arguments,
decorator_list,
type_params,
..
}) => {
self.visit_decorators(decorator_list);
if type_params.is_some() {
self.consume_scope();
}
if let Some(arguments) = arguments {
self.visit_arguments(arguments);
}
self.consume_scope();
}
ast::Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => {
if type_params.is_some() {
self.consume_scope();
}
self.consume_scope();
}
ast::Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
self.visit_expr(target);
if let Some(value) = value {
self.visit_expr(value);
}
}
ast::Stmt::If(ast::StmtIf {
test,
body,
elif_else_clauses,
..
}) => {
self.visit_expr(test);
for stmt in body {
self.visit_stmt(stmt);
}
for clause in elif_else_clauses {
if let Some(test) = &clause.test {
self.visit_expr(test);
}
for stmt in &clause.body {
self.visit_stmt(stmt);
}
}
}
ast::Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
}) => {
for stmt in body {
self.visit_stmt(stmt);
}
for handler in handlers {
self.visit_except_handler(handler);
}
for stmt in orelse {
self.visit_stmt(stmt);
}
for stmt in finalbody {
self.visit_stmt(stmt);
}
}
_ => ast::visitor::walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &ast::Expr) {
if self.error.is_some() {
return;
}
self.error = self
.compiler
.consume_skipped_nested_scopes_in_expr(expr)
.err();
}
fn visit_except_handler(&mut self, handler: &ast::ExceptHandler) {
if self.error.is_some() {
return;
}
let ast::ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
type_,
body,
..
}) = handler;
if let Some(type_) = type_ {
self.visit_expr(type_);
}
for stmt in body {
self.visit_stmt(stmt);
}
}
}
let mut visitor = SkippedStatementScopeVisitor {
compiler: self,
error: None,
};
for statement in statements {
visitor.visit_stmt(statement);
}
if let Some(err) = visitor.error {
Err(err)
} else {
Ok(())
}
}
fn consume_skipped_nested_scopes_in_except_handlers(
&mut self,
handlers: &[ast::ExceptHandler],
) -> CompileResult<()> {
use ast::visitor::Visitor;
struct SkippedHandlerScopeVisitor<'a, 'warnings> {
compiler: &'a mut Compiler<'warnings>,
error: Option<CodegenError>,
}
impl ast::visitor::Visitor<'_> for SkippedHandlerScopeVisitor<'_, '_> {
fn visit_expr(&mut self, expr: &ast::Expr) {
if self.error.is_some() {
return;
}
self.error = self
.compiler
.consume_skipped_nested_scopes_in_expr(expr)
.err();
}
fn visit_stmt(&mut self, stmt: &ast::Stmt) {
if self.error.is_some() {
return;
}
self.error = self
.compiler
.consume_skipped_nested_scopes_in_statements(slice::from_ref(stmt))
.err();
}
}
let mut visitor = SkippedHandlerScopeVisitor {
compiler: self,
error: None,
};
for handler in handlers {
visitor.visit_except_handler(handler);
if visitor.error.is_some() {
break;
}
}
if let Some(err) = visitor.error {
Err(err)
} else {
Ok(())
}
}
fn current_symbol_table_cursors(&self) -> SymbolTableCursors {
let table = self
.symbol_table_stack
.last()
.expect("no current symbol table");
SymbolTableCursors {
sub_table: table.next_sub_table,
hidden_annotation_block: table.next_hidden_annotation_block,
inlined_comprehension_block: table.next_inlined_comprehension_block,
}
}
fn set_symbol_table_cursors(&mut self, cursors: SymbolTableCursors) {
let table = self
.symbol_table_stack
.last_mut()
.expect("no current symbol table");
table.next_sub_table = cursors.sub_table;
table.next_hidden_annotation_block = cursors.hidden_annotation_block;
table.next_inlined_comprehension_block = cursors.inlined_comprehension_block;
}
/// Advance the nested-scope cursors to the first scope that begins on or
/// after `line_number`.
///
/// A statement list can be compiled more than once — `finally` bodies are
/// re-emitted on every path that leaves the try block early — and each copy
/// has to be handed the same scopes as the last. Positioning by line works
/// because `finally` is the last clause of its statement: every scope the
/// preceding clauses opened begins on an earlier line than the body being
/// re-compiled, and every scope that body opens begins on its own line or
/// later.
fn seek_symbol_table_cursors_to_line(&mut self, line_number: u32) {
fn seek(tables: &[SymbolTable], cursor: &mut usize, line_number: u32) {
while tables
.get(*cursor)
.is_some_and(|table| table.line_number < line_number)
{
*cursor += 1;
}
}
let table = self
.symbol_table_stack
.last_mut()
.expect("no current symbol table");
seek(&table.sub_tables, &mut table.next_sub_table, line_number);
seek(
&table.hidden_annotation_blocks,
&mut table.next_hidden_annotation_block,
line_number,
);
seek(
&table.inlined_comprehension_blocks,
&mut table.next_inlined_comprehension_block,
line_number,
);
}
fn lookup_comprehension_symbol_table_after_skipped_nested_scopes_in_expr(
&mut self,
expression: &ast::Expr,
comprehension_type: ComprehensionType,
) -> CompileResult<(SymbolTable, ComprehensionSymbolSource)> {
let saved_cursor = self
.symbol_table_stack
.last()
.expect("no current symbol table")
.next_sub_table;
let saved_inlined_cursor = self
.symbol_table_stack
.last()
.expect("no current symbol table")
.next_inlined_comprehension_block;
let result = (|| {
self.consume_skipped_nested_scopes_in_expr(expression)?;
let current_table = self
.symbol_table_stack
.last()
.expect("no current symbol table");
if comprehension_type != ComprehensionType::Generator
&& let Some(table) = current_table
.inlined_comprehension_blocks
.get(current_table.next_inlined_comprehension_block)
{
return Ok((table.clone(), ComprehensionSymbolSource::Inlined));
}
if let Some(table) = current_table.sub_tables.get(current_table.next_sub_table) {
Ok((table.clone(), ComprehensionSymbolSource::Child))
} else {
let name = current_table.name.clone();
let typ = current_table.typ;
Err(self.error(CodegenErrorType::SyntaxError(format!(
"no symbol table available in {name} (type: {typ:?})"
))))
}
})();
self.symbol_table_stack
.last_mut()
.expect("no current symbol table")
.next_sub_table = saved_cursor;
self.symbol_table_stack
.last_mut()
.expect("no current symbol table")
.next_inlined_comprehension_block = saved_inlined_cursor;
result
}
fn push_output_with_symbol_table(
&mut self,
table: SymbolTable,
flags: bytecode::CodeFlags,
posonlyarg_count: u32,
arg_count: u32,
kwonlyarg_count: u32,
obj_name: &str,
) -> CompileResult<()> {
let scope_type = table.typ;
self.symbol_table_stack.push(table);
let key = self.symbol_table_stack.len() - 1;
let lineno = self.get_source_line_number().get();
self.enter_scope(obj_name, scope_type, key, lineno.to_u32())?;
if let Some(info) = self.code_stack.last_mut() {
info.flags = flags
| (info.flags
& (bytecode::CodeFlags::NESTED
| bytecode::CodeFlags::METHOD
| bytecode::CodeFlags::FUTURE_DIVISION
| bytecode::CodeFlags::FUTURE_ABSOLUTE_IMPORT
| bytecode::CodeFlags::FUTURE_WITH_STATEMENT
| bytecode::CodeFlags::FUTURE_PRINT_FUNCTION
| bytecode::CodeFlags::FUTURE_UNICODE_LITERALS
| bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL
| bytecode::CodeFlags::FUTURE_GENERATOR_STOP
| bytecode::CodeFlags::FUTURE_ANNOTATIONS));
info.metadata.argcount = arg_count;
info.metadata.posonlyargcount = posonlyarg_count;
info.metadata.kwonlyargcount = kwonlyarg_count;
}
Ok(())
}
#[expect(clippy::too_many_arguments, reason = "ignore warning for now")]
fn compile_comprehension(
&mut self,
name: &str,
init_collection: Option<AnyInstruction>,
generators: &[ast::Comprehension],
compile_element: &dyn Fn(&mut Self, usize) -> CompileResult<()>,
comprehension_type: ComprehensionType,
element_contains_await: bool,
comprehension_range: TextRange,
element_range: TextRange,
outer_backedge_range: TextRange,
) -> CompileResult<()> {
let prev_ctx = self.ctx;
let has_an_async_gen = generators.iter().any(|g| g.is_async);
let is_top_level_await_context = self.opts.allow_top_level_await
&& prev_ctx.func == FunctionContext::NoFunction
&& !prev_ctx.in_class;
// Check for async comprehension outside async function (list/set/dict only, not generator expressions)
// Use in_async_scope to allow nested async comprehensions inside an async function
if comprehension_type != ComprehensionType::Generator
&& (has_an_async_gen || element_contains_await)
&& !prev_ctx.in_async_scope
&& !is_top_level_await_context
{
return Err(self.error(CodegenErrorType::InvalidAsyncComprehension));
}
// async comprehensions are allowed in various contexts:
// - list/set/dict comprehensions in async functions (or nested within)
// - always for generator expressions
let is_async_list_set_dict_comprehension = comprehension_type
!= ComprehensionType::Generator
&& (has_an_async_gen || element_contains_await)
&& (prev_ctx.in_async_scope || is_top_level_await_context);
let is_async_generator_comprehension = comprehension_type == ComprehensionType::Generator
&& (has_an_async_gen || element_contains_await);
debug_assert!(!(is_async_list_set_dict_comprehension && is_async_generator_comprehension));
let is_async = is_async_list_set_dict_comprehension || is_async_generator_comprehension;
// We must have at least one generator:
assert!(!generators.is_empty());
let outermost = &generators[0];
let (comp_table, comp_source) = self
.lookup_comprehension_symbol_table_after_skipped_nested_scopes_in_expr(
&outermost.iter,
comprehension_type,
)?;
let is_inlined = self.is_inlined_comprehension_context(comprehension_type, &comp_table);
if is_inlined {
// CPython inlines every non-generator comprehension that the
// symtable marked as comp_inlined, including async variants.
// codegen_comprehension() only branches on ste_comp_inlined here
// and relies on the inlined path itself to handle GET_AITER /
// async-comprehension cleanup.
return self.compile_inlined_comprehension(
&comp_table,
init_collection,
generators,
compile_element,
(comprehension_range, element_range, outer_backedge_range),
comp_source,
);
}
// Non-inlined path: create a new code object (generator expressions, etc.)
self.ctx = CompileContext {
in_class: prev_ctx.in_class,
func: if is_async {
FunctionContext::AsyncFunction
} else {
FunctionContext::Function
},
// Inherit in_async_scope from parent - nested async comprehensions are allowed
// if we're anywhere inside an async function
in_async_scope: prev_ctx.in_async_scope || is_async,
};
let flags = bytecode::CodeFlags::NEWLOCALS | bytecode::CodeFlags::OPTIMIZED;
let flags = if is_async {
flags | bytecode::CodeFlags::COROUTINE
} else {
flags
};
// The symbol table follows CPython's symtable walk: nested scopes
// in the outermost iterator are recorded before the comprehension
// scope itself. Peek past those nested scopes so we can enter the
// correct comprehension table here, then let the real outermost
// iterator compile consume its nested scopes later in parent scope.
self.push_output_with_symbol_table(comp_table, flags, 0, 1, 0, name)?;
// Set qualname for comprehension
self.set_qualname();
self.set_source_range(comprehension_range);
let arg0 = self.varname(".0");
let return_none = init_collection.is_none();
// CPython codegen_comprehension() wraps only generator expressions
// with codegen_wrap_in_stopiteration_handler(); unlike function bodies,
// it does not push a COMPILE_FBLOCK_STOP_ITERATION fblock.
let stop_iteration_block = if comprehension_type == ComprehensionType::Generator {
let handler_block = self.new_block();
self.insert_cpython_stopiteration_setup_cleanup(handler_block);
Some(handler_block)
} else {
None
};
// Create empty object of proper type:
if let Some(init_collection) = init_collection {
self._emit(init_collection, OpArg::new(0), BlockIdx::NULL)
}
let mut loop_labels = vec![];
let mut real_loop_depth = 0;
for (gen_index, generator) in generators.iter().enumerate() {
if gen_index > 0
&& !generator.is_async
&& let Some(singleton_iter) =
Self::singleton_comprehension_assignment_iter(&generator.iter)
{
// CPython allocates start/if_cleanup/anchor labels before the
// singleton sub-iterator fast path sets start = NO_LABEL.
let _start_label = self.current_code_info().new_instr_sequence_label();
let if_cleanup_block = self.new_block();
let _anchor_label = self.current_code_info().new_instr_sequence_label();
self.compile_expression(singleton_iter)?;
self.compile_store(&generator.target)?;
if !generator.ifs.is_empty() {
for if_condition in &generator.ifs {
self.compile_jump_if(if_condition, false, if_cleanup_block)?;
}
}
loop_labels.push(ComprehensionLoopControl::IfCleanupOnly { if_cleanup_block });
continue;
}
let loop_block = self.new_block();
let (send_block, after_block, if_cleanup_block) = if generator.is_async {
let send_block = self.new_block();
let after_block = self.new_block();
let if_cleanup_block = self.new_block();
(send_block, after_block, if_cleanup_block)
} else {
let if_cleanup_block = self.new_block();
let after_block = self.new_block();
(BlockIdx::NULL, after_block, if_cleanup_block)
};
if gen_index == 0 {
// Load iterator onto stack (passed as first argument):
emit!(self, Instruction::LoadFast { var_num: arg0 });
} else {
// Evaluate iterated item:
self.compile_comprehension_iter(generator)?;
}
self.use_cpython_label_block(loop_block);
let mut end_async_for_target = BlockIdx::NULL;
if generator.is_async {
let loop_label = self.instr_sequence_label_for_block(loop_block);
self.push_fblock_labels(
FBlockType::AsyncComprehensionGenerator,
loop_label,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)?;
emit!(self, PseudoInstruction::SetupFinally { delta: after_block });
emit!(self, Instruction::GetAnext);
self.emit_load_const(ConstantData::None);
self.use_cpython_label_block(send_block);
let _ = self.compile_yield_from_sequence(true);
end_async_for_target = send_block;
// POP_BLOCK before store: only __anext__/yield_from are
// protected by SetupFinally targeting END_ASYNC_FOR.
emit!(self, PseudoInstruction::PopBlock);
self.compile_store(&generator.target)?;
} else {
let saved_range = self.current_source_range;
self.set_source_range(generator.iter.range());
emit!(self, Instruction::ForIter { delta: after_block });
self.set_source_range(saved_range);
self.compile_store(&generator.target)?;
}
real_loop_depth += 1;
let backedge_range = if gen_index + 1 == generators.len() {
element_range
} else {
outer_backedge_range
};
loop_labels.push(ComprehensionLoopControl::Iteration {
loop_block,
if_cleanup_block,
after_block,
backedge_range,
is_async: generator.is_async,
end_async_for_target,
});
// CPython always lowers comprehension guards through codegen_jump_if
// and leaves constant-folding to later CFG optimization passes.
for if_condition in &generator.ifs {
self.compile_jump_if(if_condition, false, if_cleanup_block)?;
}
}
compile_element(self, real_loop_depth + 1)?;
for loop_control in loop_labels.iter().rev().copied() {
match loop_control {
ComprehensionLoopControl::Iteration {
loop_block,
if_cleanup_block,
after_block,
backedge_range,
is_async,
end_async_for_target,
} => {
self.use_cpython_label_block(if_cleanup_block);
self.set_source_range(backedge_range);
emit!(self, PseudoInstruction::Jump { delta: loop_block });
if is_async {
let loop_label = self.instr_sequence_label_for_block(loop_block);
self.pop_fblock_label(FBlockType::AsyncComprehensionGenerator, loop_label);
}
self.use_cpython_label_block(after_block);
if is_async {
self.set_source_range(comprehension_range);
// EndAsyncFor pops both the exception and the aiter
// (handler depth is before GetANext, so aiter is at handler depth)
self.emit_end_async_for(end_async_for_target);
} else {
self.emit_sync_comprehension_end_for();
}
}
ComprehensionLoopControl::IfCleanupOnly { if_cleanup_block } => {
self.use_cpython_label_block(if_cleanup_block);
}
}
}
if return_none {
self.emit_return_const_no_location(ConstantData::None);
} else {
self.set_source_range(comprehension_range);
self.emit_return_value();
}
// Close StopIteration handler and emit handler code
if let Some(handler_block) = stop_iteration_block {
self.use_cpython_label_block(handler_block);
emit!(
self,
Instruction::CallIntrinsic1 {
func: oparg::IntrinsicFunction1::StopIterationError
}
);
self.set_no_location();
emit!(self, Instruction::Reraise { depth: 1u32 });
self.set_no_location();
}
self.emit_return_const_no_location(ConstantData::None);
let code = self.exit_scope();
self.ctx = prev_ctx;
// Create comprehension function with closure
self.set_source_range(comprehension_range);
self.make_closure(code, bytecode::MakeFunctionFlags::new())?;
// Evaluate iterated item and get its iterator.
self.compile_comprehension_iter(outermost)?;
self.symbol_table_stack
.last_mut()
.expect("no current symbol table")
.next_sub_table += 1;
// Call just created <listcomp> function:
self.set_source_range(comprehension_range);
emit!(self, Instruction::Call { argc: 0 });
if is_async_list_set_dict_comprehension {
emit!(self, Instruction::GetAwaitable { r#where: 0 });
self.emit_load_const(ConstantData::None);
let _ = self.compile_yield_from_sequence(true);
}
Ok(())
}
/// Compile an inlined comprehension (PEP 709)
/// This generates bytecode inline without creating a new code object
fn compile_inlined_comprehension(
&mut self,
comp_table: &SymbolTable,
init_collection: Option<AnyInstruction>,
generators: &[ast::Comprehension],
compile_element: &dyn Fn(&mut Self, usize) -> CompileResult<()>,
ranges: (TextRange, TextRange, TextRange),
comp_source: ComprehensionSymbolSource,
) -> CompileResult<()> {
let (comprehension_range, element_range, outer_backedge_range) = ranges;
// Compile the outermost iterator first. Its expression may reference
// nested scopes (e.g. lambdas) whose sub_tables sit at the current
// position in the parent's list. Those must be consumed before we
// splice in the comprehension's own children.
self.compile_comprehension_iter(&generators[0])?;
match comp_source {
ComprehensionSymbolSource::Child => {
self.symbol_table_stack
.last_mut()
.expect("no current symbol table")
.next_sub_table += 1;
}
ComprehensionSymbolSource::Inlined => {
self.symbol_table_stack
.last_mut()
.expect("no current symbol table")
.next_inlined_comprehension_block += 1;
}
}
let was_in_inlined_comp = self.current_code_info().in_inlined_comp;
let saved_source_range = self.current_source_range;
let tweak_in_class_block = {
let ct = self.current_symbol_table();
ct.typ == CompilerScope::Class && !was_in_inlined_comp
};
self.current_code_info().in_inlined_comp = true;
let mut temp_symbols: IndexMap<Name, Symbol> = IndexMap::default();
let mut changed_fast_hidden = Vec::new();
let result = (|| {
// If the symbol table still carries the inlined comprehension as
// a child, splice its children here. The symtable normally
// performs this splice before codegen, and the Inlined source path
// has already done so.
if matches!(comp_source, ComprehensionSymbolSource::Child)
&& !comp_table.sub_tables.is_empty()
{
let current_table = self
.symbol_table_stack
.last_mut()
.expect("no current symbol table");
let insert_pos = current_table.next_sub_table;
for (i, st) in comp_table.sub_tables.iter().enumerate() {
current_table.sub_tables.insert(insert_pos + i, st.clone());
}
}
let mut pushed_locals: Vec<Name> = Vec::new();
let mut fast_hidden_locals: Vec<Name> = Vec::new();
for (name, sym) in &comp_table.symbols {
if sym.flags.contains(SymbolFlags::DEF_PARAM) {
continue; // skip .0
}
let is_local = sym.flags.contains(SymbolFlags::DEF_LOCAL)
&& !sym.flags.contains(SymbolFlags::DEF_NONLOCAL);
if is_local {
pushed_locals.push(name.clone());
}
if is_local || tweak_in_class_block {
fast_hidden_locals.push(name.clone());
}
}
// TweakInlinedComprehensionScopes: temporarily override parent
// symbols with comprehension scopes where they differ. For
// module/class scopes, also enable temporary fast locals for
// comprehension-bound names only.
for (name, comp_sym) in &comp_table.symbols {
if comp_sym.flags.contains(SymbolFlags::DEF_PARAM) {
continue; // skip .0
}
let comp_scope = comp_sym.scope;
let current_table = self.symbol_table_stack.last().expect("no symbol table");
if let Some(outer_sym) = current_table.symbols.get(name) {
let outer_scope = outer_sym.scope;
if (comp_scope != outer_scope
&& comp_scope != SymbolScope::Free
&& !(comp_scope == SymbolScope::Cell && outer_scope == SymbolScope::Free))
|| tweak_in_class_block
{
temp_symbols.insert(name.clone(), outer_sym.clone());
let current_table =
self.symbol_table_stack.last_mut().expect("no symbol table");
current_table.symbols.insert(name.clone(), comp_sym.clone());
}
}
}
if !self.ctx.in_func() {
for name in &fast_hidden_locals {
if self
.current_code_info()
.metadata
.fast_hidden
.get(name.as_str())
.is_none_or(|&hidden| !hidden)
{
self.current_code_info()
.metadata
.fast_hidden
.insert(name.clone().into(), true);
self.current_code_info()
.metadata
.fast_hidden_final
.insert(name.clone().into());
changed_fast_hidden.push(name.clone());
}
}
}
// Step 2: Save local variables that will be shadowed by the comprehension.
// For each variable, we push the fast local value via LoadFastAndClear.
// For merged CELL variables, LoadFastAndClear saves the cell object from
// the merged slot, and MAKE_CELL creates a new empty cell in-place.
// MAKE_CELL has no stack effect (operates only on fastlocals).
self.set_source_range(comprehension_range);
let mut total_stack_items: usize = 0;
for name in &pushed_locals {
let var_num = self.varname(name);
emit!(self, Instruction::LoadFastAndClear { var_num });
total_stack_items += 1;
// If the comp symbol is CELL, emit MAKE_CELL to create fresh cell
if let Some(comp_sym) = comp_table.symbols.get(name)
&& comp_sym.scope == SymbolScope::Cell
{
let i = if self
.current_symbol_table()
.symbols
.get(name)
.is_some_and(|s| s.scope == SymbolScope::Free)
{
self.get_free_var_index(name)
} else {
self.get_cell_var_index(name)
};
emit!(self, Instruction::MakeCell { i });
}
}
// Step 3: SWAP iterator to TOS (above saved locals + cell values)
if total_stack_items > 0 {
emit!(
self,
Instruction::Swap {
i: u32::try_from(total_stack_items + 1).unwrap()
}
);
}
// CPython's codegen_push_inlined_comprehension_locals()
// installs the virtual cleanup before codegen_comprehension()
// emits BUILD_LIST/BUILD_SET/BUILD_MAP for the result object.
let cleanup_block = if !pushed_locals.is_empty() {
let cleanup_block = self.new_block();
emit!(
self,
PseudoInstruction::SetupFinally {
delta: cleanup_block
}
);
Some(cleanup_block)
} else {
None
};
// Step 4: Create the collection (list/set/dict)
if let Some(init_collection) = init_collection {
self._emit(init_collection, OpArg::new(0), BlockIdx::NULL);
// SWAP to get iterator on top
emit!(self, Instruction::Swap { i: 2 });
}
// Step 5: Compile the comprehension loop(s)
let mut loop_labels: Vec<ComprehensionLoopControl> = vec![];
let mut real_loop_depth = 0;
for (i, generator) in generators.iter().enumerate() {
if i > 0
&& !generator.is_async
&& let Some(singleton_iter) =
Self::singleton_comprehension_assignment_iter(&generator.iter)
{
// CPython allocates start/if_cleanup/anchor labels before
// the singleton sub-iterator fast path sets start = NO_LABEL.
let _start_label = self.current_code_info().new_instr_sequence_label();
let if_cleanup_block = self.new_block();
let _anchor_label = self.current_code_info().new_instr_sequence_label();
self.compile_expression(singleton_iter)?;
self.compile_store(&generator.target)?;
if !generator.ifs.is_empty() {
for if_condition in &generator.ifs {
self.compile_jump_if(if_condition, false, if_cleanup_block)?;
}
}
loop_labels.push(ComprehensionLoopControl::IfCleanupOnly { if_cleanup_block });
continue;
}
let loop_block = self.new_block();
let (send_block, after_block, if_cleanup_block) = if generator.is_async {
let send_block = self.new_block();
let after_block = self.new_block();
let if_cleanup_block = self.new_block();
(send_block, after_block, if_cleanup_block)
} else {
let if_cleanup_block = self.new_block();
let after_block = self.new_block();
(BlockIdx::NULL, after_block, if_cleanup_block)
};
if i > 0 {
self.compile_comprehension_iter(generator)?;
}
self.use_cpython_label_block(loop_block);
let mut end_async_for_target = BlockIdx::NULL;
if generator.is_async {
let loop_label = self.instr_sequence_label_for_block(loop_block);
self.push_fblock_labels(
FBlockType::AsyncComprehensionGenerator,
loop_label,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)?;
emit!(self, PseudoInstruction::SetupFinally { delta: after_block });
emit!(self, Instruction::GetAnext);
self.emit_load_const(ConstantData::None);
self.use_cpython_label_block(send_block);
let _ = self.compile_yield_from_sequence(true);
end_async_for_target = send_block;
emit!(self, PseudoInstruction::PopBlock);
self.compile_store(&generator.target)?;
} else {
let saved_range = self.current_source_range;
self.set_source_range(generator.iter.range());
emit!(self, Instruction::ForIter { delta: after_block });
self.set_source_range(saved_range);
self.compile_store(&generator.target)?;
}
real_loop_depth += 1;
let backedge_range = if i + 1 == generators.len() {
element_range
} else {
outer_backedge_range
};
loop_labels.push(ComprehensionLoopControl::Iteration {
loop_block,
if_cleanup_block,
after_block,
backedge_range,
is_async: generator.is_async,
end_async_for_target,
});
// CPython always lowers comprehension guards through codegen_jump_if
// and leaves constant-folding to later CFG optimization passes.
for if_condition in &generator.ifs {
self.compile_jump_if(if_condition, false, if_cleanup_block)?;
}
}
// Step 6: Compile the element expression and append to collection
compile_element(self, real_loop_depth + 1)?;
// Step 7: Close all loops
for loop_control in loop_labels.iter().rev().copied() {
match loop_control {
ComprehensionLoopControl::Iteration {
loop_block,
if_cleanup_block,
after_block,
backedge_range,
is_async,
end_async_for_target,
} => {
self.use_cpython_label_block(if_cleanup_block);
self.set_source_range(backedge_range);
emit!(self, PseudoInstruction::Jump { delta: loop_block });
if is_async {
let loop_label = self.instr_sequence_label_for_block(loop_block);
self.pop_fblock_label(
FBlockType::AsyncComprehensionGenerator,
loop_label,
);
}
self.use_cpython_label_block(after_block);
if is_async {
self.set_source_range(comprehension_range);
self.emit_end_async_for(end_async_for_target);
} else {
self.emit_sync_comprehension_end_for();
}
}
ComprehensionLoopControl::IfCleanupOnly { if_cleanup_block } => {
self.use_cpython_label_block(if_cleanup_block);
}
}
}
// Step 8: Clean up - restore saved locals (and cell values)
self.set_source_range(comprehension_range);
if let Some(cleanup_block) = cleanup_block {
emit!(self, PseudoInstruction::PopBlock);
self.set_no_location();
// Match CPython codegen_pop_inlined_comprehension_locals():
// the synthetic jump that skips the exception cleanup uses
// JUMP_NO_INTERRUPT, which becomes JUMP_BACKWARD_NO_INTERRUPT
// when the cleanup tail sits above the final restore block.
let end_block = self.new_block();
emit!(
self,
PseudoInstruction::JumpNoInterrupt { delta: end_block }
);
self.set_no_location();
// Exception cleanup path
self.use_cpython_label_block(cleanup_block);
// Stack: [saved_values..., collection, exception]
emit!(self, Instruction::Swap { i: 2 });
self.set_no_location();
emit!(self, Instruction::PopTop); // Pop incomplete collection
self.set_no_location();
// Restore locals and cell values
self.set_source_range(comprehension_range);
emit!(
self,
Instruction::Swap {
i: u32::try_from(total_stack_items + 1).unwrap()
}
);
for name in pushed_locals.iter().rev() {
let var_num = self.varname(name).as_u32();
emit!(self, PseudoInstruction::StoreFastMaybeNull { var_num });
}
// Re-raise the exception
emit!(self, Instruction::Reraise { depth: 0 });
self.set_no_location();
// Normal end path
self.use_cpython_label_block(end_block);
self.set_source_range(comprehension_range);
}
// SWAP result to TOS (above saved values)
if total_stack_items > 0 {
emit!(
self,
Instruction::Swap {
i: u32::try_from(total_stack_items + 1).unwrap()
}
);
}
// Restore saved locals (StoreFast restores the saved cell object for merged cells)
for name in pushed_locals.iter().rev() {
let var_num = self.varname(name).as_u32();
emit!(self, PseudoInstruction::StoreFastMaybeNull { var_num });
}
self.set_source_range(saved_source_range);
Ok(())
})();
let current_table = self.symbol_table_stack.last_mut().expect("no symbol table");
for (name, original_sym) in temp_symbols {
current_table.symbols.insert(name, original_sym);
}
for name in changed_fast_hidden {
self.current_code_info()
.metadata
.fast_hidden
.insert(name.into(), false);
}
self.current_code_info().in_inlined_comp = was_in_inlined_comp;
result
}
fn compile_future_features(&mut self, features: &[ast::Alias]) -> Result<(), CodegenError> {
for feature in features {
let future_feature = feature.name.as_str().try_into().map_err(|name| {
self.error_ranged(CodegenErrorType::InvalidFutureFeature(name), feature.range)
})?;
match future_feature {
FutureFeature::Braces => {
return Err(
self.error_ranged(CodegenErrorType::InvalidFutureBraces, feature.range)
);
}
FutureFeature::Annotations => {
self.future_annotations = true;
self.future_features
.insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
self.current_code_info()
.flags
.insert(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
}
FutureFeature::BarryAsFLUFL => {
self.future_features
.insert(bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL);
self.current_code_info()
.flags
.insert(bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL);
}
FutureFeature::AbsoluteImport
| FutureFeature::Division
| FutureFeature::GeneratorStop
| FutureFeature::Generators
| FutureFeature::NestedScopes
| FutureFeature::PrintFunction
| FutureFeature::UnicodeLiterals
| FutureFeature::WithStatement => {
// Python 3 features. They are already implemented by default.
}
}
}
Ok(())
}
// Low level helper functions:
/// CPython `_PyCfgBuilder_Addop()`: start a new basic block if the current
/// one is terminated, then append the instruction to the current block.
fn cpython_cfg_builder_addop(&mut self, info: ir::InstructionInfo) {
self.maybe_start_cpython_cfg_addop_block();
self.push_emitted_instruction(info);
}
fn push_emitted_instruction(&mut self, info: ir::InstructionInfo) {
self.current_code_info()
.addop_to_instr_sequence(info)
.expect("malformed instruction sequence emission");
self.current_code_info()
.addop_to_current_block(info)
.expect("malformed CFG emission");
}
fn push_emitted_instruction_with_target_label(
&mut self,
info: ir::InstructionInfo,
target_label: ir::InstructionSequenceLabel,
) {
self.current_code_info()
.addop_to_instr_sequence_with_target_label(info, target_label)
.expect("malformed instruction sequence emission");
self.current_code_info()
.addop_to_current_block(info)
.expect("malformed CFG emission");
}
fn last_emitted_instruction_mut(&mut self) -> Option<&mut ir::InstructionInfo> {
self.current_code_info().last_current_block_instr_mut()
}
fn set_last_emitted_lineno_override(&mut self, lineno_override: i32) {
self.current_code_info()
.set_last_instr_sequence_lineno_override(lineno_override);
if let Some(last) = self.last_emitted_instruction_mut() {
last.lineno_override = Some(lineno_override);
}
}
fn _emit<I: Into<AnyInstruction>>(&mut self, instr: I, arg: OpArg, target: BlockIdx) {
if self.do_not_emit_bytecode > 0 {
return;
}
let instr = instr.into();
let opcode = AnyOpcode::from(instr);
debug_assert!(
!instr.is_assembler(),
"CPython codegen_addop_* must not emit assembler-only opcodes"
);
debug_assert!(
opcode.has_arg() || instr.has_target() || u32::from(arg) == 0,
"CPython _PyInstructionSequence_Addop requires either OPCODE_HAS_ARG, HAS_TARGET, or oparg == 0"
);
debug_assert!(
target == BlockIdx::NULL || instr.has_target(),
"CPython codegen_addop_j only accepts HAS_TARGET opcodes"
);
let range = self.current_source_range;
let source = self.source_file.to_source_code();
let location = source.source_location(range.start(), PositionEncoding::Utf8);
let end_location = source.source_location(range.end(), PositionEncoding::Utf8);
let except_handler = None;
self.cpython_cfg_builder_addop(ir::InstructionInfo {
instr,
arg,
target,
location,
end_location,
except_handler,
lineno_override: None,
});
}
/// CPython `codegen_addop_j()`: emit a HAS_TARGET instruction with a
/// jump_target_label oparg.
fn emit_jump_label<I: Into<AnyInstruction>>(
&mut self,
instr: I,
target_label: ir::InstructionSequenceLabel,
) {
if self.do_not_emit_bytecode > 0 {
return;
}
let instr = instr.into();
debug_assert!(
instr.has_target(),
"CPython codegen_addop_j only accepts HAS_TARGET opcodes"
);
debug_assert!(
!instr.is_assembler(),
"CPython codegen_addop_j must not emit assembler-only opcodes"
);
let range = self.current_source_range;
let source = self.source_file.to_source_code();
let location = source.source_location(range.start(), PositionEncoding::Utf8);
let end_location = source.source_location(range.end(), PositionEncoding::Utf8);
let target = self
.current_code_info()
.block_for_instr_sequence_label(target_label);
self.maybe_start_cpython_cfg_addop_block();
self.push_emitted_instruction_with_target_label(
ir::InstructionInfo {
instr,
arg: OpArg::NULL,
target,
location,
end_location,
except_handler: None,
lineno_override: None,
},
target_label,
);
}
/// Mark the last emitted instruction as having no source location.
/// Prevents it from triggering LINE events in sys.monitoring.
fn set_no_location(&mut self) {
self.set_last_emitted_lineno_override(-1);
}
/// CPython `codegen_sync_comprehension_generator()` emits END_FOR/POP_ITER
/// with NO_LOCATION and lets `flowgraph.c::propagate_line_numbers()` copy
/// the FOR_ITER location onto the cleanup block.
fn emit_sync_comprehension_end_for(&mut self) {
emit!(self, Instruction::EndFor);
self.set_no_location();
emit!(self, Instruction::PopIter);
self.set_no_location();
}
/// CPython `codegen_wrap_in_stopiteration_handler()` inserts
/// `SETUP_CLEANUP` into the instruction sequence at index 0 with
/// `_PyInstructionSequence_InsertInstruction()`. Generator and cell/free
/// prefixes are inserted later by `flowgraph.c::insert_prefix_instructions()`.
fn insert_cpython_stopiteration_setup_cleanup(&mut self, handler_block: BlockIdx) {
let code = self.current_code_info();
let entry = code
.blocks
.first_mut()
.expect("code unit must have an entry block");
debug_assert!(
entry
.used_instructions()
.first()
.is_some_and(|info| match info.instr.real() {
Some(Instruction::Resume { context }) => matches!(
context.get(info.arg).location(),
oparg::ResumeLocation::AtFuncStart
),
_ => false,
}),
"scope entry must start with a function-start RESUME"
);
debug_assert!(
!entry.used_instructions().iter().any(|info| matches!(
info.instr.real(),
Some(
Instruction::ReturnGenerator
| Instruction::MakeCell { .. }
| Instruction::CopyFreeVars { .. }
)
)),
"CPython inserts StopIteration cleanup before CFG prefix instructions"
);
let result = code.insert_start_setup_cleanup(handler_block);
unwrap_internal(self, result);
}
fn emit_no_arg<I: Into<AnyInstruction>>(&mut self, ins: I) {
self._emit(ins, OpArg::NULL, BlockIdx::NULL)
}
fn emit_arg<A: OpArgType, T: EmitArg<A>, I: Into<AnyInstruction>>(
&mut self,
arg: T,
f: impl FnOnce(OpArgMarker<A>) -> I,
) {
let (op, arg, target) = arg.emit(f);
self._emit(op, arg, target)
}
// fn block_done()
fn arg_constant(&mut self, constant: ConstantData) -> oparg::ConstIdx {
let info = self.current_code_info();
if let ConstantData::Code { code } = &constant
&& let Some(idx) = info.metadata.consts.iter().position(|existing| {
matches!(
existing,
ConstantData::Code {
code: existing_code
} if Self::code_objects_equivalent(existing_code, code)
)
})
{
return u32::try_from(idx)
.expect("constant table index overflow")
.into();
}
info.metadata.consts.insert_full(constant).0.to_u32().into()
}
fn constants_equivalent(lhs: &ConstantData, rhs: &ConstantData) -> bool {
match (lhs, rhs) {
(ConstantData::Code { code: lhs }, ConstantData::Code { code: rhs }) => {
Self::code_objects_equivalent(lhs, rhs)
}
(ConstantData::Tuple { elements: lhs }, ConstantData::Tuple { elements: rhs })
| (
ConstantData::Frozenset { elements: lhs },
ConstantData::Frozenset { elements: rhs },
) => {
lhs.len() == rhs.len()
&& lhs
.iter()
.zip(rhs.iter())
.all(|(lhs, rhs)| Self::constants_equivalent(lhs, rhs))
}
(ConstantData::Slice { elements: lhs }, ConstantData::Slice { elements: rhs }) => lhs
.iter()
.zip(rhs.iter())
.all(|(lhs, rhs)| Self::constants_equivalent(lhs, rhs)),
_ => lhs == rhs,
}
}
fn code_objects_equivalent(lhs: &bytecode::CodeObject, rhs: &bytecode::CodeObject) -> bool {
lhs.instructions.len() == rhs.instructions.len()
&& lhs
.instructions
.iter()
.zip(rhs.instructions.iter())
.all(|(lhs, rhs)| u8::from(lhs.op) == u8::from(rhs.op) && lhs.arg == rhs.arg)
&& lhs.locations == rhs.locations
&& lhs.flags.bits() == rhs.flags.bits()
&& lhs.posonlyarg_count == rhs.posonlyarg_count
&& lhs.arg_count == rhs.arg_count
&& lhs.kwonlyarg_count == rhs.kwonlyarg_count
&& lhs.source_path == rhs.source_path
&& lhs.first_line_number == rhs.first_line_number
&& lhs.max_stackdepth == rhs.max_stackdepth
&& lhs.obj_name == rhs.obj_name
&& lhs.qualname == rhs.qualname
&& lhs.constants.len() == rhs.constants.len()
&& lhs
.constants
.iter()
.zip(rhs.constants.iter())
.all(|(lhs, rhs)| Self::constants_equivalent(lhs, rhs))
&& lhs.names == rhs.names
&& lhs.varnames == rhs.varnames
&& lhs.cellvars == rhs.cellvars
&& lhs.freevars == rhs.freevars
&& lhs.localspluskinds == rhs.localspluskinds
&& lhs.linetable == rhs.linetable
&& lhs.exceptiontable == rhs.exceptiontable
}
fn constant_as_fold_int(constant: &ConstantData) -> Option<(BigInt, bool)> {
match constant {
ConstantData::Boolean { value } => Some((BigInt::from(u8::from(*value)), true)),
ConstantData::Integer { value } => Some((value.clone(), false)),
_ => None,
}
}
fn try_fold_constant_binop(
op: ast::Operator,
left: &ConstantData,
right: &ConstantData,
) -> Option<ConstantData> {
let (left_int, left_is_bool) = Self::constant_as_fold_int(left)?;
let (right_int, right_is_bool) = Self::constant_as_fold_int(right)?;
if !(left_is_bool && right_is_bool) {
return None;
}
match op {
ast::Operator::BitAnd => Some(ConstantData::Boolean {
value: !left_int.is_zero() & !right_int.is_zero(),
}),
ast::Operator::BitOr => Some(ConstantData::Boolean {
value: !left_int.is_zero() | !right_int.is_zero(),
}),
ast::Operator::BitXor => Some(ConstantData::Boolean {
value: !left_int.is_zero() ^ !right_int.is_zero(),
}),
_ => None,
}
}
fn try_fold_constant_expr(&mut self, expr: &ast::Expr) -> CompileResult<Option<ConstantData>> {
if let Some(constant) = self.ast_constant_value(expr) {
return Ok(Some(constant));
}
Ok(Some(match expr {
ast::Expr::NumberLiteral(num) => match &num.value {
ast::Number::Int(int) => ConstantData::Integer {
value: ruff_int_to_bigint(int).map_err(|e| self.error(e))?,
},
ast::Number::Float(f) => ConstantData::Float { value: *f },
ast::Number::Complex { real, imag } => ConstantData::Complex {
value: Complex::new(*real, *imag),
},
},
ast::Expr::StringLiteral(s) => ConstantData::Str {
value: string_literal_value(&self.source_file, &s.value),
},
ast::Expr::BytesLiteral(b) => ConstantData::Bytes {
value: b.value.bytes().collect(),
},
ast::Expr::BooleanLiteral(b) => ConstantData::Boolean { value: b.value },
ast::Expr::NoneLiteral(_) => ConstantData::None,
ast::Expr::EllipsisLiteral(_) => ConstantData::Ellipsis,
ast::Expr::Name(ast::ExprName { id, ctx, .. })
if matches!(ctx, ast::ExprContext::Load) && id.as_str() == "__debug__" =>
{
ConstantData::Boolean {
value: self.opts.optimize == 0,
}
}
ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
let mut elements = Vec::with_capacity(elts.len());
for elt in elts {
let Some(constant) = self.try_fold_constant_expr(elt)? else {
return Ok(None);
};
elements.push(constant);
}
ConstantData::Tuple { elements }
}
ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
let Some(container) = self.try_fold_constant_expr(value)? else {
return Ok(None);
};
let Some(index) = self.try_fold_constant_expr(slice)? else {
return Ok(None);
};
let ConstantData::Integer { value: index } = index else {
return Ok(None);
};
let Some(index): Option<i64> = index.try_into().ok() else {
return Ok(None);
};
match container {
ConstantData::Str { value } => {
let string = value.to_string();
if string.contains(char::REPLACEMENT_CHARACTER) {
return Ok(None);
}
let chars: Vec<_> = string.chars().collect();
let Some(len) = i64::try_from(chars.len()).ok() else {
return Ok(None);
};
let idx: i64 = if index < 0 { len + index } else { index };
let Some(idx) = usize::try_from(idx).ok() else {
return Ok(None);
};
let Some(ch) = chars.get(idx) else {
return Ok(None);
};
ConstantData::Str {
value: ch.to_string().into(),
}
}
ConstantData::Bytes { value } => {
let Some(len) = i64::try_from(value.len()).ok() else {
return Ok(None);
};
let idx: i64 = if index < 0 { len + index } else { index };
let Some(idx) = usize::try_from(idx).ok() else {
return Ok(None);
};
let Some(byte) = value.get(idx) else {
return Ok(None);
};
ConstantData::Integer {
value: BigInt::from(*byte),
}
}
ConstantData::Tuple { elements } => {
let Some(len) = i64::try_from(elements.len()).ok() else {
return Ok(None);
};
let idx: i64 = if index < 0 { len + index } else { index };
let Some(idx) = usize::try_from(idx).ok() else {
return Ok(None);
};
let Some(element) = elements.get(idx) else {
return Ok(None);
};
element.clone()
}
_ => return Ok(None),
}
}
ast::Expr::BinOp(ast::ExprBinOp {
left, op, right, ..
}) => {
let Some(left) = self.try_fold_constant_expr(left)? else {
return Ok(None);
};
let Some(right) = self.try_fold_constant_expr(right)? else {
return Ok(None);
};
let Some(constant) = Self::try_fold_constant_binop(*op, &left, &right) else {
return Ok(None);
};
constant
}
ast::Expr::UnaryOp(ast::ExprUnaryOp { op, operand, .. }) => {
let Some(constant) = self.try_fold_constant_expr(operand)? else {
return Ok(None);
};
match (op, constant) {
(ast::UnaryOp::UAdd, value) => value,
(ast::UnaryOp::USub, ConstantData::Integer { value }) => {
ConstantData::Integer { value: -value }
}
(ast::UnaryOp::USub, ConstantData::Float { value }) => {
ConstantData::Float { value: -value }
}
(ast::UnaryOp::USub, ConstantData::Complex { value }) => {
ConstantData::Complex { value: -value }
}
(ast::UnaryOp::Invert, ConstantData::Integer { value }) => {
ConstantData::Integer { value: !value }
}
(ast::UnaryOp::Not, ConstantData::Tuple { .. }) => return Ok(None),
(ast::UnaryOp::Not, value) => ConstantData::Boolean {
value: !Self::constant_truthiness(&value),
},
_ => return Ok(None),
}
}
ast::Expr::BoolOp(ast::ExprBoolOp { op, values, .. }) => {
let mut constants = Vec::with_capacity(values.len());
for value in values {
let Some(constant) = self.try_fold_constant_expr(value)? else {
return Ok(None);
};
constants.push(constant);
}
let mut iter = constants.into_iter();
let Some(first) = iter.next() else {
return Ok(None);
};
let mut selected = first;
match op {
ast::BoolOp::Or => {
if !Self::constant_truthiness(&selected) {
for constant in iter {
let is_truthy = Self::constant_truthiness(&constant);
selected = constant;
if is_truthy {
break;
}
}
}
}
ast::BoolOp::And => {
if Self::constant_truthiness(&selected) {
for constant in iter {
let is_truthy = Self::constant_truthiness(&constant);
selected = constant;
if !is_truthy {
break;
}
}
}
}
}
selected
}
_ => return Ok(None),
}))
}
fn try_compile_ast_constant(
&mut self,
expr: &ast::Expr,
) -> CompileResult<Option<ConstantData>> {
if let Some(constant) = self.ast_constant_value(expr) {
return Ok(Some(constant));
}
Ok(Some(match expr {
ast::Expr::NumberLiteral(num) => match &num.value {
ast::Number::Int(int) => ConstantData::Integer {
value: ruff_int_to_bigint(int).map_err(|e| self.error(e))?,
},
ast::Number::Float(value) => ConstantData::Float { value: *value },
ast::Number::Complex { real, imag } => ConstantData::Complex {
value: Complex::new(*real, *imag),
},
},
ast::Expr::StringLiteral(s) => ConstantData::Str {
value: string_literal_value(&self.source_file, &s.value),
},
ast::Expr::BytesLiteral(b) => ConstantData::Bytes {
value: b.value.bytes().collect(),
},
ast::Expr::BooleanLiteral(b) => ConstantData::Boolean { value: b.value },
ast::Expr::NoneLiteral(_) => ConstantData::None,
ast::Expr::EllipsisLiteral(_) => ConstantData::Ellipsis,
_ => return Ok(None),
}))
}
fn try_compile_match_mapping_key_direct_constant(
&mut self,
expr: &ast::Expr,
) -> CompileResult<Option<ConstantData>> {
if let Some(constant) = self.ast_constant_value(expr) {
return Ok(match constant {
ConstantData::Integer { .. }
| ConstantData::Float { .. }
| ConstantData::Bytes { .. }
| ConstantData::Complex { .. }
| ConstantData::Str { .. }
| ConstantData::Boolean { .. }
| ConstantData::None => Some(constant),
_ => None,
});
}
if matches!(
expr,
ast::Expr::BooleanLiteral(_) | ast::Expr::NoneLiteral(_)
) {
return self.try_compile_ast_constant(expr);
}
self.try_compile_match_pattern_direct_literal(expr)
}
fn is_unexpected_match_literal_constant(expr: &ast::Expr) -> bool {
if let Some(constant) = expr
.as_constant_expr()
.map(|expr| ast_constant_value_to_constant_data(expr.value.clone()))
{
return matches!(
constant,
ConstantData::Boolean { .. } | ConstantData::None | ConstantData::Ellipsis
);
}
matches!(
expr,
ast::Expr::BooleanLiteral(_)
| ast::Expr::NoneLiteral(_)
| ast::Expr::EllipsisLiteral(_)
)
}
fn try_compile_match_pattern_direct_literal(
&mut self,
expr: &ast::Expr,
) -> CompileResult<Option<ConstantData>> {
if let Some(constant) = self.ast_constant_value(expr) {
return Ok(match constant {
ConstantData::Integer { .. }
| ConstantData::Float { .. }
| ConstantData::Bytes { .. }
| ConstantData::Complex { .. }
| ConstantData::Str { .. } => Some(constant),
_ => None,
});
}
match expr {
ast::Expr::NumberLiteral(_)
| ast::Expr::StringLiteral(_)
| ast::Expr::BytesLiteral(_) => self.try_compile_ast_constant(expr),
_ => Ok(None),
}
}
fn try_negate_match_pattern_constant(constant: ConstantData) -> Option<ConstantData> {
match constant {
ConstantData::Integer { value } => Some(ConstantData::Integer { value: -value }),
ConstantData::Float { value } => Some(ConstantData::Float { value: -value }),
ConstantData::Complex { value } => Some(ConstantData::Complex { value: -value }),
_ => None,
}
}
fn try_fold_match_pattern_binop(
op: ast::Operator,
left: &ConstantData,
right: &ConstantData,
) -> Option<ConstantData> {
let left = match left {
ConstantData::Integer { value } => value.to_f64()?,
ConstantData::Float { value } => *value,
_ => return None,
};
let ConstantData::Complex { value: right } = right else {
return None;
};
let value = match op {
ast::Operator::Add => Complex::new(left + right.re, right.im),
ast::Operator::Sub => Complex::new(left - right.re, -right.im),
_ => return None,
};
Some(ConstantData::Complex { value })
}
fn try_fold_match_pattern_const_expr(
&mut self,
expr: &ast::Expr,
) -> CompileResult<Option<ConstantData>> {
// CPython 3.14 ast_preprocess.c::fold_const_match_patterns()
// folds only the constant forms needed by match patterns before
// codegen_pattern_value()/codegen_pattern_mapping_key() visit them.
Ok(match expr {
ast::Expr::UnaryOp(ast::ExprUnaryOp {
op: ast::UnaryOp::USub,
operand,
..
}) => {
let Some(constant) = self.try_compile_match_pattern_number_constant(operand)?
else {
return Ok(None);
};
Self::try_negate_match_pattern_constant(constant)
}
ast::Expr::BinOp(ast::ExprBinOp {
left, op, right, ..
}) if matches!(op, ast::Operator::Add | ast::Operator::Sub) => {
let Some(left) = self.try_compile_match_pattern_signed_real_constant(left)? else {
return Ok(None);
};
let Some(right) = self.try_compile_match_pattern_imaginary_constant(right)? else {
return Ok(None);
};
Self::try_fold_match_pattern_binop(*op, &left, &right)
}
_ => None,
})
}
fn try_compile_match_pattern_signed_real_constant(
&mut self,
expr: &ast::Expr,
) -> CompileResult<Option<ConstantData>> {
if let Some(constant) = self.try_compile_match_pattern_real_constant(expr)? {
return Ok(Some(constant));
}
let ast::Expr::UnaryOp(ast::ExprUnaryOp {
op: ast::UnaryOp::USub,
operand,
..
}) = expr
else {
return Ok(None);
};
let Some(constant) = self.try_compile_match_pattern_real_constant(operand)? else {
return Ok(None);
};
Ok(Self::try_negate_match_pattern_constant(constant))
}
fn try_compile_match_pattern_real_constant(
&mut self,
expr: &ast::Expr,
) -> CompileResult<Option<ConstantData>> {
let Some(constant) = self.try_compile_match_pattern_number_constant(expr)? else {
return Ok(None);
};
Ok(match constant {
ConstantData::Integer { .. } | ConstantData::Float { .. } => Some(constant),
_ => None,
})
}
fn try_compile_match_pattern_imaginary_constant(
&mut self,
expr: &ast::Expr,
) -> CompileResult<Option<ConstantData>> {
let Some(constant) = self.try_compile_match_pattern_number_constant(expr)? else {
return Ok(None);
};
Ok(match constant {
ConstantData::Complex { .. } => Some(constant),
_ => None,
})
}
fn try_compile_match_pattern_number_constant(
&mut self,
expr: &ast::Expr,
) -> CompileResult<Option<ConstantData>> {
if let Some(constant) = self.ast_constant_value(expr) {
return Ok(match constant {
ConstantData::Integer { .. }
| ConstantData::Float { .. }
| ConstantData::Complex { .. } => Some(constant),
_ => None,
});
}
match expr {
ast::Expr::NumberLiteral(_) => self.try_compile_ast_constant(expr),
_ => Ok(None),
}
}
fn compile_match_pattern_expr(&mut self, expr: &ast::Expr) -> CompileResult<()> {
if let Some(constant) = self.try_fold_match_pattern_const_expr(expr)? {
self.set_source_range(expr.range());
self.emit_load_const(constant);
} else {
self.compile_expression(expr)?;
}
Ok(())
}
fn emit_load_const(&mut self, constant: ConstantData) {
let idx = self.arg_constant(constant);
self.emit_arg(idx, |consti| Instruction::LoadConst { consti })
}
fn try_fold_constant_slice(
&mut self,
lower: Option<&ast::Expr>,
upper: Option<&ast::Expr>,
step: Option<&ast::Expr>,
) -> CompileResult<Option<ConstantData>> {
if [lower, upper, step]
.into_iter()
.flatten()
.any(|expr| !self.is_constant_expr(expr))
{
return Ok(None);
}
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,
};
Ok(Some(ConstantData::Slice {
elements: Box::new([start, stop, step]),
}))
}
fn emit_return_const(&mut self, constant: ConstantData) {
self.emit_load_const(constant);
emit!(self, Instruction::ReturnValue)
}
fn emit_return_const_no_location(&mut self, constant: ConstantData) {
self.emit_load_const(constant);
self.set_no_location();
emit!(self, Instruction::ReturnValue);
self.set_no_location();
}
fn emit_end_async_for(&mut self, send_target: BlockIdx) {
self._emit(Instruction::EndAsyncFor, OpArg::NULL, send_target);
}
/// Emit LOAD_ATTR for attribute access (method=false).
/// Encodes: (name_idx << 1) | 0
fn emit_load_attr(&mut self, name_idx: u32) {
let encoded = LoadAttr::new(name_idx, false);
self.emit_arg(encoded, |namei| Instruction::LoadAttr { namei })
}
/// Emit LOAD_ATTR with method flag set (for method calls).
/// Encodes: (name_idx << 1) | 1
fn emit_load_attr_method(&mut self, name_idx: u32) {
let encoded = LoadAttr::new(name_idx, true);
self.emit_arg(encoded, |namei| Instruction::LoadAttr { namei })
}
/// Emit LOAD_GLOBAL.
/// Encodes: (name_idx << 1) | push_null_bit
fn emit_load_global(&mut self, name_idx: u32, push_null: bool) {
let encoded = (name_idx << 1) | u32::from(push_null);
self.emit_arg(encoded, |namei| Instruction::LoadGlobal { namei });
}
/// Emit LOAD_SUPER_ATTR for 2-arg super().attr access.
/// Encodes: (name_idx << 2) | 0b10 (method=0, class=1)
fn emit_load_super_attr(&mut self, name_idx: u32) {
let encoded = LoadSuperAttr::new(name_idx, false, true);
self.emit_arg(encoded, |namei| Instruction::LoadSuperAttr { namei })
}
/// Emit LOAD_SUPER_ATTR for 2-arg super().method() call.
/// Encodes: (name_idx << 2) | 0b11 (method=1, class=1)
fn emit_load_super_method(&mut self, name_idx: u32) {
let encoded = LoadSuperAttr::new(name_idx, true, true);
self.emit_arg(encoded, |namei| Instruction::LoadSuperAttr { namei })
}
/// Emit LOAD_SUPER_ATTR for 0-arg super().attr access.
/// Encodes: (name_idx << 2) | 0b00 (method=0, class=0)
fn emit_load_zero_super_attr(&mut self, name_idx: u32) {
let encoded = LoadSuperAttr::new(name_idx, false, false);
self.emit_arg(encoded, |namei| Instruction::LoadSuperAttr { namei })
}
/// Emit LOAD_SUPER_ATTR for 0-arg super().method() call.
/// Encodes: (name_idx << 2) | 0b01 (method=1, class=0)
fn emit_load_zero_super_method(&mut self, name_idx: u32) {
let encoded = LoadSuperAttr::new(name_idx, true, false);
self.emit_arg(encoded, |namei| Instruction::LoadSuperAttr { namei })
}
fn emit_return_value(&mut self) {
emit!(self, Instruction::ReturnValue)
}
fn allows_top_level_await_in_current_context(&self) -> bool {
self.opts.allow_top_level_await
&& self.ctx.func == FunctionContext::NoFunction
&& !self.ctx.in_class
}
fn current_code_info(&mut self) -> &mut ir::CodeInfo {
self.code_stack.last_mut().expect("no code on stack")
}
/// Enter a conditional block (if/for/while/match/try/with)
/// PEP 649: Track conditional annotation context
fn enter_conditional_block(&mut self) {
self.current_code_info().in_conditional_block += 1;
}
/// Leave a conditional block
fn leave_conditional_block(&mut self) {
let code_info = self.current_code_info();
debug_assert!(code_info.in_conditional_block > 0);
code_info.in_conditional_block -= 1;
}
/// Compile break or continue statement with proper fblock cleanup.
/// compiler_break, compiler_continue
/// This handles unwinding through With blocks and exception handlers.
fn compile_break_continue(
&mut self,
range: ruff_text_size::TextRange,
is_break: bool,
) -> CompileResult<()> {
if self.do_not_emit_bytecode > 0 {
// Still validate that we're inside a loop even in dead code
let code = self.current_code_info();
let mut found_loop = false;
for i in (0..code.fblock.len()).rev() {
match code.fblock[i].fb_type {
FBlockType::WhileLoop | FBlockType::ForLoop => {
found_loop = true;
break;
}
FBlockType::ExceptionGroupHandler => {
return Err(self.error_ranged(
CodegenErrorType::BreakContinueReturnInExceptStar,
range,
));
}
_ => {}
}
}
if !found_loop {
if is_break {
return Err(self.error_ranged(CodegenErrorType::InvalidBreak, range));
}
return Err(self.error_ranged(CodegenErrorType::InvalidContinue, range));
}
return Ok(());
}
let prev_source_range = self.current_source_range;
self.set_source_range(range);
emit!(self, Instruction::Nop);
let (mut unwind_loc, loop_fblock) = self.unwind_fblock_stack_with_loop(false, true)?;
let Some(loop_fblock) = loop_fblock else {
self.set_source_range(prev_source_range);
if is_break {
return Err(self.error_ranged(CodegenErrorType::InvalidBreak, range));
}
return Err(self.error_ranged(CodegenErrorType::InvalidContinue, range));
};
// CPython `codegen_break()` unwinds the loop fblock itself after
// `codegen_unwind_fblock_stack()` returns it. `codegen_continue()`
// jumps directly to the loop body label.
if is_break {
self.unwind_fblock(&loop_fblock, false, &mut unwind_loc)?;
}
// Jump to target
let target_label = if is_break {
debug_assert!(loop_fblock.fb_exit.is_jump_target_label());
loop_fblock.fb_exit
} else {
debug_assert!(loop_fblock.fb_block.is_jump_target_label());
loop_fblock.fb_block
};
let jump_is_artificial = if let Some(loc) = unwind_loc {
self.set_source_range(loc);
false
} else {
true
};
self.emit_jump_label(
PseudoInstruction::Jump {
delta: OpArgMarker::marker(),
},
target_label,
);
if jump_is_artificial {
self.set_no_location();
}
self.set_source_range(prev_source_range);
Ok(())
}
/// CPython `_PyCfgBuilder_Addop()` calls
/// `cfg_builder_maybe_start_new_block()` before appending each instruction.
/// That keeps any `IS_TERMINATOR_OPCODE` as the final instruction in its
/// basicblock before `flowgraph.c::check_cfg()`.
fn maybe_start_cpython_cfg_addop_block(&mut self) {
let code = self.current_code_info();
let cur = code.current_block;
if !code.blocks[cur.idx()]
.used_instructions()
.last()
.is_some_and(|instr| instr.instr.is_terminator())
{
return;
}
debug_assert_eq!(code.blocks[cur.idx()].next, BlockIdx::NULL);
let block = self.cpython_cfg_builder_new_block();
self.cpython_cfg_builder_use_next_block(block);
}
/// CPython `codegen_funcbody()` emits `NEW_JUMP_TARGET_LABEL(start)` and
/// `USE_LABEL(start)` after scope entry. That label is part of the
/// instruction-sequence label map, but it does not become a CFG
/// `basicblock.b_label` unless some emitted instruction targets it.
fn use_cpython_function_start_label(&mut self) -> ir::InstructionSequenceLabel {
let (label, result) = {
let code = self.current_code_info();
let label = code.new_instr_sequence_label();
let result = code.use_raw_instr_sequence_label(label);
(label, result)
};
unwrap_internal(self, result);
label
}
fn instr_sequence_label_for_block(&mut self, block: BlockIdx) -> ir::InstructionSequenceLabel {
let result = self
.current_code_info()
.instr_sequence_label_for_block(block);
unwrap_internal(self, result)
}
/// Switch to a block as CPython instruction-sequence labels would resolve.
///
/// Consecutive `USE_LABEL()` calls can map multiple labels to the same
/// instruction offset before `_PyCfg_FromInstructionSequence()` builds a
/// CFG. Reuse an empty current block even if it already carries a label so
/// codegen CFG path preserves that aliasing.
fn use_cpython_label_block(&mut self, block: BlockIdx) {
let result = self.current_code_info().use_instr_sequence_label(block);
unwrap_internal(self, result);
let code = self.current_code_info();
let block = code.resolve_instr_sequence_label(block);
let cur = code.current_block;
let can_reuse_current = cur != block
&& code.blocks[cur.idx()].is_empty()
&& code.blocks[cur.idx()].next == BlockIdx::NULL
&& code.blocks[block.idx()].is_empty()
&& code.blocks[block.idx()].next == BlockIdx::NULL;
if !can_reuse_current {
let result = code.mark_cpython_cfg_label(block);
unwrap_internal(self, result);
self.switch_to_block(block);
return;
}
{
let target = &code.blocks[block.idx()];
debug_assert!(!target.except_handler);
debug_assert!(!target.preserve_lasti);
debug_assert_eq!(target.start_depth, ir::START_DEPTH_UNSET);
debug_assert!(!target.cold);
}
let result = code.mark_cpython_cfg_label(cur);
unwrap_internal(self, result);
let result = self
.current_code_info()
.use_instr_sequence_label_at_block(block, cur);
unwrap_internal(self, result);
}
fn new_block(&mut self) -> BlockIdx {
let result = self
.current_code_info()
.blocks
.try_reserve(1)
.map_err(|_| InternalError::MalformedControlFlowGraph);
unwrap_internal(self, result);
let code = self.current_code_info();
let idx = BlockIdx::new(code.blocks.len().to_u32());
code.blocks.push(Block::default());
let result = code.push_unmapped_instr_sequence_label();
unwrap_internal(self, result);
idx
}
fn new_unlabeled_block(&mut self) -> BlockIdx {
let result = self
.current_code_info()
.blocks
.try_reserve(1)
.map_err(|_| InternalError::MalformedControlFlowGraph);
unwrap_internal(self, result);
let code = self.current_code_info();
let idx = BlockIdx::new(code.blocks.len().to_u32());
code.blocks.push(Block::default());
let result = code.push_unlabeled_instr_sequence_block();
unwrap_internal(self, result);
idx
}
/// flowgraph.c cfg_builder_new_block
fn cpython_cfg_builder_new_block(&mut self) -> BlockIdx {
self.new_unlabeled_block()
}
/// flowgraph.c cfg_builder_use_next_block
fn cpython_cfg_builder_use_next_block(&mut self, block: BlockIdx) {
let code = self.current_code_info();
let cur = code.current_block;
code.blocks[cur.idx()].next = block;
code.current_block = block;
}
fn switch_to_block(&mut self, block: BlockIdx) {
let result = self.current_code_info().use_instr_sequence_label(block);
unwrap_internal(self, result);
let code = self.current_code_info();
let block = code.resolve_instr_sequence_label(block);
let prev = code.current_block;
assert_ne!(prev, block, "recursive switching {prev:?} -> {block:?}");
assert_eq!(
code.blocks[block].next,
BlockIdx::NULL,
"switching {prev:?} -> {block:?} to completed block"
);
let prev_block = &mut code.blocks[prev.idx()];
assert_eq!(
u32::from(prev_block.next),
u32::MAX,
"switching {prev:?} -> {block:?} from block that's already got a next"
);
prev_block.next = block;
code.current_block = block;
}
const fn set_source_range(&mut self, range: TextRange) {
self.current_source_range = range;
}
fn update_start_location_to_match_attr(
&self,
loc_range: TextRange,
attr_range: TextRange,
attr: &str,
) -> TextRange {
let source = self.source_file.to_source_code();
if source.line_index(loc_range.start()) == source.line_index(attr_range.end()) {
return loc_range;
}
let Ok(attr_len) = u32::try_from(attr.chars().count()) else {
return TextRange::new(loc_range.start(), loc_range.end());
};
let attr_len = TextSize::new(attr_len);
if attr_len > attr_range.len() {
return TextRange::new(loc_range.start(), loc_range.end());
}
TextRange::new(attr_range.end() - attr_len, loc_range.end())
}
fn source_line_start_range(&self, lineno: u32) -> TextRange {
let source = self.source_file.to_source_code();
let line = OneIndexed::new(lineno as usize).unwrap_or(OneIndexed::MIN);
let start = source.line_start(line);
TextRange::new(start, start)
}
fn module_start_location(&self, body: &[ast::Stmt]) -> TextRange {
body.first()
.map_or_else(|| self.source_line_start_range(1), Ranged::range)
}
fn get_source_line_number(&mut self) -> OneIndexed {
self.source_file
.to_source_code()
.line_index(self.current_source_range.start())
}
fn mark_generator(&mut self) {
let is_async = self.ctx.func == FunctionContext::AsyncFunction;
let flags = &mut self.current_code_info().flags;
if is_async {
flags.remove(bytecode::CodeFlags::COROUTINE);
flags.insert(bytecode::CodeFlags::ASYNC_GENERATOR);
} else {
flags.insert(bytecode::CodeFlags::GENERATOR);
}
}
/// Whether the expression contains an await expression and
/// thus requires the function to be async.
///
/// Both:
/// ```py
/// async with: ...
/// async for: ...
/// ```
/// are statements, so we won't check for them here
fn contains_await(expression: &ast::Expr) -> bool {
use ast::visitor::Visitor;
#[derive(Default)]
struct AwaitVisitor {
found: bool,
}
impl ast::visitor::Visitor<'_> for AwaitVisitor {
fn visit_expr(&mut self, expr: &ast::Expr) {
if self.found {
return;
}
match expr {
ast::Expr::Await(_) => self.found = true,
ast::Expr::ListComp(ast::ExprListComp { generators, .. })
| ast::Expr::SetComp(ast::ExprSetComp { generators, .. })
| ast::Expr::DictComp(ast::ExprDictComp { generators, .. })
if generators.iter().any(|generator| generator.is_async) =>
{
self.found = true
}
_ => ast::visitor::walk_expr(self, expr),
}
}
}
let mut visitor = AwaitVisitor::default();
visitor.visit_expr(expression);
visitor.found
}
/// Check if any of the generators (except the first one's iter) contains an await expression.
/// The first generator's iter is evaluated outside the comprehension scope.
fn generators_contain_await(generators: &[ast::Comprehension]) -> bool {
for (i, generator) in generators.iter().enumerate() {
// First generator's iter is evaluated outside the comprehension
if i > 0 && Self::contains_await(&generator.iter) {
return true;
}
// Check ifs in all generators
for if_expr in &generator.ifs {
if Self::contains_await(if_expr) {
return true;
}
}
}
false
}
fn compile_expr_fstring(&mut self, fstring: &ast::ExprFString) -> CompileResult<()> {
let fstring_range = fstring.range;
let fstring = fstring.value.as_slice();
if self.count_fstring_parts(fstring) > STACK_USE_GUIDELINE {
return self.compile_fstring_parts_joined(fstring, fstring_range);
}
let mut element_count = 0;
let mut pending_literal = None;
let mut pending_literal_range = None;
let mut pending_literal_no_location = false;
for part in fstring {
self.compile_fstring_part_into(
part,
&mut pending_literal,
&mut pending_literal_range,
&mut pending_literal_no_location,
&mut element_count,
None,
)?;
}
self.finish_fstring(
pending_literal,
pending_literal_range,
pending_literal_no_location,
element_count,
Some(fstring_range),
);
Ok(())
}
fn compile_runtime_joined_str(
&mut self,
fstring: &ast::ExprFString,
values: &[ast::Expr],
) -> CompileResult<()> {
let range = fstring.range;
let value_count: u32 = values
.len()
.try_into()
.expect("JoinedStr value count overflowed");
if value_count > STACK_USE_GUIDELINE {
self.set_source_range(range);
self.emit_load_const(ConstantData::Str {
value: Wtf8Buf::new(),
});
let join_idx = self.get_global_name_index("join");
self.emit_load_attr_method(join_idx);
emit!(self, Instruction::BuildList { count: 0 });
for value in values {
self.compile_expression(value)?;
self.set_source_range(range);
emit!(self, Instruction::ListAppend { i: 1 });
}
self.set_source_range(range);
emit!(self, Instruction::Call { argc: 1 });
} else {
for value in values {
self.compile_expression(value)?;
}
if value_count > 1 {
self.set_source_range(range);
emit!(self, Instruction::BuildString { count: value_count });
} else if value_count == 0 {
self.set_source_range(range);
self.emit_load_const(ConstantData::Str {
value: Wtf8Buf::new(),
});
}
}
Ok(())
}
fn compile_fstring_parts_joined(
&mut self,
fstring: &[ast::FStringPart],
fstring_range: TextRange,
) -> CompileResult<()> {
self.set_source_range(fstring_range);
self.emit_load_const(ConstantData::Str {
value: Wtf8Buf::new(),
});
let join_idx = self.get_global_name_index("join");
self.emit_load_attr_method(join_idx);
emit!(self, Instruction::BuildList { count: 0 });
let mut element_count = 0;
let mut pending_literal = None;
let mut pending_literal_range = None;
let mut pending_literal_no_location = false;
for part in fstring {
self.compile_fstring_part_into(
part,
&mut pending_literal,
&mut pending_literal_range,
&mut pending_literal_no_location,
&mut element_count,
Some(fstring_range),
)?;
}
self.finish_fstring_join(
pending_literal,
pending_literal_range,
pending_literal_no_location,
element_count,
fstring_range,
);
Ok(())
}
fn compile_fstring_part_into(
&mut self,
part: &ast::FStringPart,
pending_literal: &mut Option<Wtf8Buf>,
pending_literal_range: &mut Option<TextRange>,
pending_literal_no_location: &mut bool,
element_count: &mut u32,
join_append_range: Option<TextRange>,
) -> CompileResult<()> {
match part {
ast::FStringPart::Literal(string) => {
let value = string_literal_part_value(&self.source_file, string);
if pending_literal.is_none() {
*pending_literal_range = Some(string.range);
*pending_literal_no_location = string.range == TextRange::default();
*pending_literal = Some(value);
} else if let Some(pending) = pending_literal.as_mut() {
Self::extend_pending_literal_range(pending_literal_range, string.range);
*pending_literal_no_location &= string.range == TextRange::default();
pending.push_wtf8(value.as_ref());
}
Ok(())
}
ast::FStringPart::FString(fstring) => self.compile_fstring_elements_into(
fstring.flags,
&fstring.elements,
pending_literal,
(pending_literal_range, pending_literal_no_location),
element_count,
join_append_range,
),
}
}
fn finish_fstring(
&mut self,
mut pending_literal: Option<Wtf8Buf>,
mut pending_literal_range: Option<TextRange>,
mut pending_literal_no_location: bool,
mut element_count: u32,
fstring_range: Option<TextRange>,
) {
self.emit_pending_fstring_literal(
&mut pending_literal,
&mut pending_literal_range,
&mut pending_literal_no_location,
&mut element_count,
None,
);
if element_count == 0 {
if let Some(fstring_range) = fstring_range {
self.set_source_range(fstring_range);
}
self.emit_load_const(ConstantData::Str {
value: Wtf8Buf::new(),
});
} else if element_count > 1 {
if let Some(fstring_range) = fstring_range {
self.set_source_range(fstring_range);
}
emit!(
self,
Instruction::BuildString {
count: element_count
}
);
}
}
fn finish_fstring_join(
&mut self,
mut pending_literal: Option<Wtf8Buf>,
mut pending_literal_range: Option<TextRange>,
mut pending_literal_no_location: bool,
mut element_count: u32,
fstring_range: TextRange,
) {
self.emit_pending_fstring_literal(
&mut pending_literal,
&mut pending_literal_range,
&mut pending_literal_no_location,
&mut element_count,
Some(fstring_range),
);
self.set_source_range(fstring_range);
emit!(self, Instruction::Call { argc: 1 });
}
fn emit_pending_fstring_literal(
&mut self,
pending_literal: &mut Option<Wtf8Buf>,
pending_literal_range: &mut Option<TextRange>,
pending_literal_no_location: &mut bool,
element_count: &mut u32,
join_append_range: Option<TextRange>,
) {
let Some(value) = pending_literal.take() else {
return;
};
let range = pending_literal_range.take();
let no_location = *pending_literal_no_location;
*pending_literal_no_location = false;
// An empty literal fragment contributes nothing, so it is dropped. An
// f-string left with no fragments at all still loads an empty string,
// positioned at the whole f-string rather than at any one fragment.
if value.is_empty() {
return;
}
if let Some(range) = range {
self.set_source_range(range);
}
self.emit_load_const(ConstantData::Str { value });
if no_location {
self.set_no_location();
}
*element_count += 1;
if let Some(join_append_range) = join_append_range {
self.set_source_range(join_append_range);
emit!(self, Instruction::ListAppend { i: 1 });
}
}
fn extend_pending_literal_range(pending: &mut Option<TextRange>, range: TextRange) {
let Some(existing) = pending else {
*pending = Some(range);
return;
};
if *existing == TextRange::default() {
*existing = range;
} else if range != TextRange::default() {
*existing = TextRange::new(existing.start(), range.end());
}
}
fn count_fstring_parts(&self, fstring: &[ast::FStringPart]) -> u32 {
let mut element_count = 0;
let mut pending_literal = None;
for part in fstring {
self.count_fstring_part_into(part, &mut pending_literal, &mut element_count);
}
Self::count_pending_fstring_literal(&mut pending_literal, &mut element_count);
element_count
}
fn count_fstring_part_into(
&self,
part: &ast::FStringPart,
pending_literal: &mut Option<Wtf8Buf>,
element_count: &mut u32,
) {
match part {
ast::FStringPart::Literal(string) => {
let value = string_literal_part_value(&self.source_file, string);
if let Some(pending) = pending_literal.as_mut() {
pending.push_wtf8(value.as_ref());
} else {
*pending_literal = Some(value);
}
}
ast::FStringPart::FString(fstring) => self.count_fstring_elements_into(
fstring.flags,
&fstring.elements,
pending_literal,
element_count,
),
}
}
fn count_pending_fstring_literal(
pending_literal: &mut Option<Wtf8Buf>,
element_count: &mut u32,
) {
let Some(value) = pending_literal.take() else {
return;
};
if value.is_empty() {
return;
}
*element_count += 1;
}
fn compile_fstring_elements(
&mut self,
flags: ast::FStringFlags,
fstring_elements: &ast::InterpolatedStringElements,
fstring_range: Option<TextRange>,
) -> CompileResult<()> {
if self.count_fstring_elements(flags, fstring_elements) > STACK_USE_GUIDELINE {
let fstring_range = fstring_range.unwrap_or(self.current_source_range);
return self.compile_fstring_elements_joined(flags, fstring_elements, fstring_range);
}
let mut element_count = 0;
let mut pending_literal: Option<Wtf8Buf> = None;
let mut pending_literal_range: Option<TextRange> = None;
let mut pending_literal_no_location = false;
self.compile_fstring_elements_into(
flags,
fstring_elements,
&mut pending_literal,
(&mut pending_literal_range, &mut pending_literal_no_location),
&mut element_count,
None,
)?;
self.finish_fstring(
pending_literal,
pending_literal_range,
pending_literal_no_location,
element_count,
fstring_range,
);
Ok(())
}
fn compile_fstring_elements_joined(
&mut self,
flags: ast::FStringFlags,
fstring_elements: &ast::InterpolatedStringElements,
fstring_range: TextRange,
) -> CompileResult<()> {
self.set_source_range(fstring_range);
self.emit_load_const(ConstantData::Str {
value: Wtf8Buf::new(),
});
let join_idx = self.get_global_name_index("join");
self.emit_load_attr_method(join_idx);
emit!(self, Instruction::BuildList { count: 0 });
let mut element_count = 0;
let mut pending_literal: Option<Wtf8Buf> = None;
let mut pending_literal_range: Option<TextRange> = None;
let mut pending_literal_no_location = false;
self.compile_fstring_elements_into(
flags,
fstring_elements,
&mut pending_literal,
(&mut pending_literal_range, &mut pending_literal_no_location),
&mut element_count,
Some(fstring_range),
)?;
self.finish_fstring_join(
pending_literal,
pending_literal_range,
pending_literal_no_location,
element_count,
fstring_range,
);
Ok(())
}
fn cpython_format_spec_range(&self, range: TextRange) -> TextRange {
let start = range.start().to_usize();
if start == 0 {
return range;
}
let source = self.source_file.source_text().as_bytes();
if source.get(start - 1) == Some(&b':') {
TextRange::new(range.start() - TextSize::new(1), range.end())
} else {
range
}
}
fn compile_fstring_elements_into(
&mut self,
flags: ast::FStringFlags,
fstring_elements: &ast::InterpolatedStringElements,
pending_literal: &mut Option<Wtf8Buf>,
pending_literal_meta: (&mut Option<TextRange>, &mut bool),
element_count: &mut u32,
join_append_range: Option<TextRange>,
) -> CompileResult<()> {
let (pending_literal_range, pending_literal_no_location) = pending_literal_meta;
for element in fstring_elements {
match element {
ast::InterpolatedStringElement::Literal(string) => {
let value =
interpolated_string_literal_value(&self.source_file, string, flags.into());
if pending_literal.is_none() {
*pending_literal_range = Some(string.range);
*pending_literal_no_location = string.range == TextRange::default();
*pending_literal = Some(value);
} else if let Some(pending) = pending_literal.as_mut() {
Self::extend_pending_literal_range(pending_literal_range, string.range);
*pending_literal_no_location &= string.range == TextRange::default();
pending.push_wtf8(value.as_ref());
}
}
ast::InterpolatedStringElement::Interpolation(fstring_expr) => {
let mut conversion = match fstring_expr.conversion {
ast::ConversionFlag::None => ConvertValueOparg::None,
ast::ConversionFlag::Str => ConvertValueOparg::Str,
ast::ConversionFlag::Repr => ConvertValueOparg::Repr,
ast::ConversionFlag::Ascii => ConvertValueOparg::Ascii,
};
if let Some(debug_text) = &fstring_expr.debug_text {
let (text, debug_text_range) = interpolation_debug_text(
&self.source_file,
debug_text,
fstring_expr.expression.range(),
);
let text: Wtf8Buf = text.into();
Self::extend_pending_literal_range(pending_literal_range, debug_text_range);
*pending_literal_no_location = false;
pending_literal
.get_or_insert_with(Wtf8Buf::new)
.push_wtf8(text.as_ref());
// If debug text is present, apply repr conversion when no `format_spec` specified.
// See action_helpers.c: fstring_find_expr_replacement
if matches!(
(conversion, &fstring_expr.format_spec),
(ConvertValueOparg::None, None)
) {
conversion = ConvertValueOparg::Repr;
}
}
self.emit_pending_fstring_literal(
pending_literal,
pending_literal_range,
pending_literal_no_location,
element_count,
join_append_range,
);
self.compile_expression(&fstring_expr.expression)?;
let formatted_value_range = fstring_expr.range;
match conversion {
ConvertValueOparg::None => {}
ConvertValueOparg::Str
| ConvertValueOparg::Repr
| ConvertValueOparg::Ascii => {
self.set_source_range(formatted_value_range);
emit!(self, Instruction::ConvertValue { oparg: conversion })
}
}
if let Some(format_spec) =
fstring_expr.runtime_formatted_value_format_spec.as_deref()
{
self.compile_expression(format_spec)?;
self.set_source_range(formatted_value_range);
emit!(self, Instruction::FormatWithSpec);
} else {
match &fstring_expr.format_spec {
Some(format_spec) => {
let format_spec_range =
self.cpython_format_spec_range(format_spec.range);
self.compile_fstring_elements(
flags,
&format_spec.elements,
Some(format_spec_range),
)?;
self.set_source_range(formatted_value_range);
emit!(self, Instruction::FormatWithSpec);
}
None => {
self.set_source_range(formatted_value_range);
emit!(self, Instruction::FormatSimple);
}
}
}
*element_count += 1;
if let Some(join_append_range) = join_append_range {
self.set_source_range(join_append_range);
emit!(self, Instruction::ListAppend { i: 1 });
}
}
}
}
Ok(())
}
fn count_fstring_elements(
&self,
flags: ast::FStringFlags,
fstring_elements: &ast::InterpolatedStringElements,
) -> u32 {
let mut element_count = 0;
let mut pending_literal = None;
self.count_fstring_elements_into(
flags,
fstring_elements,
&mut pending_literal,
&mut element_count,
);
Self::count_pending_fstring_literal(&mut pending_literal, &mut element_count);
element_count
}
fn count_fstring_elements_into(
&self,
flags: ast::FStringFlags,
fstring_elements: &ast::InterpolatedStringElements,
pending_literal: &mut Option<Wtf8Buf>,
element_count: &mut u32,
) {
for element in fstring_elements {
match element {
ast::InterpolatedStringElement::Literal(string) => {
let value =
interpolated_string_literal_value(&self.source_file, string, flags.into());
if let Some(pending) = pending_literal.as_mut() {
pending.push_wtf8(value.as_ref());
} else {
*pending_literal = Some(value);
}
}
ast::InterpolatedStringElement::Interpolation(fstring_expr) => {
if let Some(debug_text) = &fstring_expr.debug_text {
let (text, _) = interpolation_debug_text(
&self.source_file,
debug_text,
fstring_expr.expression.range(),
);
let text: Wtf8Buf = text.into();
pending_literal
.get_or_insert_with(Wtf8Buf::new)
.push_wtf8(text.as_ref());
}
Self::count_pending_fstring_literal(pending_literal, element_count);
*element_count += 1;
}
}
}
}
fn compile_expr_tstring(&mut self, expr_tstring: &ast::ExprTString) -> CompileResult<()> {
// ast::TStringValue can contain multiple ast::TString parts (implicit
// concatenation). Match CPython's stack order by materializing the
// strings tuple first, then evaluating interpolations left-to-right.
let tstring_value = &expr_tstring.value;
let mut all_strings: Vec<(Wtf8Buf, TextRange)> = Vec::new();
let mut current_string = Wtf8Buf::new();
let mut current_string_range = None;
let mut interp_count: u32 = 0;
for tstring in tstring_value {
self.collect_tstring_strings(
tstring,
&mut all_strings,
&mut current_string,
&mut current_string_range,
&mut interp_count,
expr_tstring.range,
);
}
all_strings.push((
core::mem::take(&mut current_string),
current_string_range.unwrap_or(expr_tstring.range),
));
let string_count: u32 = all_strings
.len()
.try_into()
.expect("t-string string count overflowed");
for (s, range) in &all_strings {
self.set_source_range(*range);
self.emit_load_const(ConstantData::Str { value: s.clone() });
}
self.set_source_range(expr_tstring.range);
emit!(
self,
Instruction::BuildTuple {
count: string_count
}
);
for tstring in tstring_value {
self.compile_tstring_interpolations(tstring)?;
}
self.set_source_range(expr_tstring.range);
emit!(
self,
Instruction::BuildTuple {
count: interp_count
}
);
self.set_source_range(expr_tstring.range);
emit!(self, Instruction::BuildTemplate);
Ok(())
}
fn compile_runtime_template_str(
&mut self,
expr_tstring: &ast::ExprTString,
values: &[ast::Expr],
) -> CompileResult<()> {
let mut last_was_interpolation = true;
let mut strings_len = 0;
for value in values {
if self.runtime_template_value_interpolation(value).is_some() {
if last_was_interpolation {
self.set_source_range(expr_tstring.range);
self.emit_load_const(ConstantData::Str {
value: Wtf8Buf::new(),
});
strings_len += 1;
}
last_was_interpolation = true;
} else {
self.compile_expression(value)?;
strings_len += 1;
last_was_interpolation = false;
}
}
if last_was_interpolation {
self.set_source_range(expr_tstring.range);
self.emit_load_const(ConstantData::Str {
value: Wtf8Buf::new(),
});
strings_len += 1;
}
self.set_source_range(expr_tstring.range);
emit!(self, Instruction::BuildTuple { count: strings_len });
let mut interpolations_len = 0;
for value in values {
if let Some((tstring, interpolation)) = self.runtime_template_value_interpolation(value)
{
self.compile_runtime_interpolation(tstring, interpolation)?;
interpolations_len += 1;
}
}
self.set_source_range(expr_tstring.range);
emit!(
self,
Instruction::BuildTuple {
count: interpolations_len
}
);
self.set_source_range(expr_tstring.range);
emit!(self, Instruction::BuildTemplate);
Ok(())
}
fn runtime_template_value_interpolation<'a>(
&self,
value: &'a ast::Expr,
) -> Option<(
&'a ast::ExprTString,
(&'a ast::ConstantValue, Option<&'a ast::Expr>),
)> {
let ast::Expr::TString(tstring) = value else {
return None;
};
let interpolation = Self::single_runtime_interpolation(tstring)?;
Self::single_tstring_interpolation(tstring)?;
Some((tstring, interpolation))
}
fn compile_runtime_interpolation(
&mut self,
expr_tstring: &ast::ExprTString,
interpolation: (&ast::ConstantValue, Option<&ast::Expr>),
) -> CompileResult<bool> {
let Some(interp) = Self::single_tstring_interpolation(expr_tstring) else {
return Ok(false);
};
self.compile_interpolation(interp, interpolation)?;
Ok(true)
}
fn compile_interpolation(
&mut self,
interp: &ast::InterpolatedElement,
interpolation: (&ast::ConstantValue, Option<&ast::Expr>),
) -> CompileResult<()> {
let (str, format_spec) = interpolation;
self.compile_expression(&interp.expression)?;
self.set_source_range(interp.range);
self.emit_load_const(ast_constant_value_to_constant_data(str.clone()));
let conversion = match interp.conversion {
ast::ConversionFlag::None => 0,
ast::ConversionFlag::Str => 1,
ast::ConversionFlag::Repr => 2,
ast::ConversionFlag::Ascii => 3,
};
let has_format_spec = format_spec.is_some();
if let Some(format_spec) = format_spec {
self.compile_expression(format_spec)?;
}
let format = 2 | (conversion << 2) | u32::from(has_format_spec);
self.set_source_range(interp.range);
emit!(self, Instruction::BuildInterpolation { format });
Ok(())
}
fn single_tstring_interpolation(
expr_tstring: &ast::ExprTString,
) -> Option<&ast::InterpolatedElement> {
let [tstring] = expr_tstring.value.as_slice() else {
return None;
};
let mut elements = tstring.elements.iter();
let ast::InterpolatedStringElement::Interpolation(interp) = elements.next()? else {
return None;
};
if elements.next().is_some() {
return None;
}
Some(interp)
}
fn collect_tstring_strings(
&self,
tstring: &ast::TString,
strings: &mut Vec<(Wtf8Buf, TextRange)>,
current_string: &mut Wtf8Buf,
current_string_range: &mut Option<TextRange>,
interp_count: &mut u32,
template_range: TextRange,
) {
for element in &tstring.elements {
match element {
ast::InterpolatedStringElement::Literal(lit) => {
if current_string_range.is_none() {
*current_string_range = Some(lit.range);
} else {
Self::extend_pending_literal_range(current_string_range, lit.range);
}
current_string.push_wtf8(&interpolated_string_literal_value(
&self.source_file,
lit,
tstring.flags.into(),
));
}
ast::InterpolatedStringElement::Interpolation(interp) => {
if let Some(debug_text) = &interp.debug_text {
let (text, debug_text_range) = interpolation_debug_text(
&self.source_file,
debug_text,
interp.expression.range(),
);
if current_string_range.is_none() {
*current_string_range = Some(debug_text_range);
} else {
Self::extend_pending_literal_range(
current_string_range,
debug_text_range,
);
}
current_string.push_str(&text);
strings.push((
core::mem::take(current_string),
current_string_range.take().unwrap_or(template_range),
));
} else {
strings.push((
core::mem::take(current_string),
current_string_range.take().unwrap_or(template_range),
));
}
*interp_count += 1;
}
}
}
}
fn compile_tstring_interpolations(&mut self, tstring: &ast::TString) -> CompileResult<()> {
for element in &tstring.elements {
let ast::InterpolatedStringElement::Interpolation(interp) = element else {
continue;
};
if let Some(runtime_str) = interp.runtime_str.as_ref() {
let interpolation = (
runtime_str,
interp.runtime_interpolation_format_spec.as_deref(),
);
self.compile_interpolation(interp, interpolation)?;
continue;
}
self.compile_expression(&interp.expression)?;
let expr_range = interp.expression.range();
let expr_source = if interp.range.start() < expr_range.start()
&& interp.range.end() >= expr_range.end()
{
let after_brace = interp.range.start() + TextSize::new(1);
self.source_file
.source_text()
.slice(TextRange::new(after_brace, expr_range.end()))
} else {
self.source_file.source_text().slice(expr_range)
}
.to_string();
self.set_source_range(interp.range);
self.emit_load_const(ConstantData::Str {
value: expr_source.into(),
});
let mut conversion: u32 = match interp.conversion {
ast::ConversionFlag::None => 0,
ast::ConversionFlag::Str => 1,
ast::ConversionFlag::Repr => 2,
ast::ConversionFlag::Ascii => 3,
};
if interp.debug_text.is_some() && conversion == 0 && interp.format_spec.is_none() {
conversion = 2;
}
let has_format_spec = interp.format_spec.is_some();
if let Some(format_spec) = &interp.format_spec {
let format_spec_range = self.cpython_format_spec_range(format_spec.range);
self.compile_fstring_elements(
ast::FStringFlags::empty(),
&format_spec.elements,
Some(format_spec_range),
)?;
}
// CPython keeps bit 1 set in BUILD_INTERPOLATION's oparg and uses
// bit 0 for the optional format spec.
let format = 2 | (conversion << 2) | u32::from(has_format_spec);
self.set_source_range(interp.range);
emit!(self, Instruction::BuildInterpolation { format });
}
Ok(())
}
}
trait EmitArg<Arg: OpArgType> {
fn emit<I: Into<AnyInstruction>>(
self,
f: impl FnOnce(OpArgMarker<Arg>) -> I,
) -> (AnyInstruction, OpArg, BlockIdx);
}
impl<T: OpArgType> EmitArg<T> for T {
fn emit<I: Into<AnyInstruction>>(
self,
f: impl FnOnce(OpArgMarker<T>) -> I,
) -> (AnyInstruction, OpArg, BlockIdx) {
let (marker, arg) = OpArgMarker::new(self);
(f(marker).into(), arg, BlockIdx::NULL)
}
}
impl EmitArg<bytecode::Label> for BlockIdx {
fn emit<I: Into<AnyInstruction>>(
self,
f: impl FnOnce(OpArgMarker<bytecode::Label>) -> I,
) -> (AnyInstruction, OpArg, BlockIdx) {
(f(OpArgMarker::marker()).into(), OpArg::NULL, self)
}
}
/// Strips leading whitespace from a docstring.
///
/// `inspect.cleandoc` is a good reference, but has a few incompatibilities.
// = _PyCompile_CleanDoc
fn clean_doc(doc: &str) -> String {
let doc = expandtabs(doc, 8);
// First pass: find minimum indentation of non-blank lines AFTER the first line.
// A "blank line" is one containing only spaces (or empty).
let margin = doc
.split('\n')
.skip(1) // skip first line
.filter(|line| line.chars().any(|c| c != ' ')) // non-blank lines only
.map(|line| line.chars().take_while(|c| *c == ' ').count())
.min()
.unwrap_or(0);
let mut cleaned = String::with_capacity(doc.len());
// Strip all leading spaces from the first line
if let Some(first_line) = doc.split('\n').next() {
let trimmed = first_line.trim_start();
// Early exit: no leading spaces on first line AND margin == 0
if trimmed.len() == first_line.len() && margin == 0 {
return doc.to_owned();
}
cleaned.push_str(trimmed);
}
// Subsequent lines: skip up to `margin` leading spaces
for line in doc.split('\n').skip(1) {
cleaned.push('\n');
let skip = line.chars().take(margin).take_while(|c| *c == ' ').count();
cleaned.push_str(&line[skip..]);
}
cleaned
}
// copied from rustpython_common::str, so we don't have to depend on it just for this function
fn expandtabs(input: &str, tab_size: usize) -> String {
let tab_stop = tab_size;
let mut expanded_str = String::with_capacity(input.len());
let mut tab_size = tab_stop;
let mut col_count = 0usize;
for ch in input.chars() {
match ch {
'\t' => {
let num_spaces = tab_size - col_count;
col_count += num_spaces;
let expand = " ".repeat(num_spaces);
expanded_str.push_str(&expand);
}
'\r' | '\n' => {
expanded_str.push(ch);
col_count = 0;
tab_size = 0;
}
_ => {
expanded_str.push(ch);
col_count += 1;
}
}
if col_count >= tab_size {
tab_size += tab_stop;
}
}
expanded_str
}
fn split_doc_with_range<'a>(
body: &'a [ast::Stmt],
opts: &CompileOpts,
) -> (Option<(String, TextRange)>, &'a [ast::Stmt]) {
if let Some((ast::Stmt::Expr(expr), body_rest)) = body.split_first() {
let doc_comment: Option<(&str, TextRange)> = match &*expr.value {
ast::Expr::StringLiteral(value) => Some((value.value.to_str(), expr.value.range())),
ast::Expr::Constant(ast::ExprConstant {
value: ast::ConstantValue::Str(value),
..
}) => Some((value.as_ref(), expr.value.range())),
// f-strings are not allowed in Python doc comments.
ast::Expr::FString(_) => None,
_ => None,
};
if let Some((doc, range)) = doc_comment {
return if opts.optimize < 2 {
(Some((clean_doc(doc), range)), body_rest)
} else {
(None, body_rest)
};
}
}
(None, body)
}
#[cfg(test)]
fn split_doc<'a>(body: &'a [ast::Stmt], opts: &CompileOpts) -> (Option<String>, &'a [ast::Stmt]) {
let (doc, body) = split_doc_with_range(body, opts);
(doc.map(|(doc, _)| doc), body)
}
pub fn ruff_int_to_bigint(int: &ast::Int) -> Result<BigInt, CodegenErrorType> {
if let Some(small) = int.as_u64() {
Ok(BigInt::from(small))
} else {
parse_big_integer(int)
}
}
/// Converts a `ruff` ast integer into a `BigInt`.
/// Unlike small integers, big integers may be stored in one of four possible radix representations.
fn parse_big_integer(int: &ast::Int) -> Result<BigInt, CodegenErrorType> {
// TODO: Improve ruff API
// Can we avoid this copy?
let s = format!("{int}");
let mut s = s.as_str();
// See: https://peps.python.org/pep-0515/#literal-grammar
let radix = match s.get(0..2) {
Some("0b" | "0B") => {
s = s.get(2..).unwrap_or(s);
2
}
Some("0o" | "0O") => {
s = s.get(2..).unwrap_or(s);
8
}
Some("0x" | "0X") => {
s = s.get(2..).unwrap_or(s);
16
}
_ => 10,
};
BigInt::from_str_radix(s, radix).map_err(|e| {
CodegenErrorType::SyntaxError(format!(
"unparsed integer literal (radix {radix}): {s} ({e})"
))
})
}
// Note: Not a good practice in general. Keep this trait private only for compiler
trait ToU32 {
fn to_u32(self) -> u32;
}
impl ToU32 for usize {
fn to_u32(self) -> u32 {
self.try_into().unwrap()
}
}
#[cfg(test)]
mod ruff_tests {
use super::*;
use ast::name::Name;
/// Test if the compiler can correctly identify fstrings containing an `await` expression.
#[test]
fn fstring_contains_await() {
let range = TextRange::default();
let flags = ast::FStringFlags::empty();
// f'{x}'
let expr_x = ast::Expr::Name(ast::ExprName {
node_index: ast::AtomicNodeIndex::NONE,
range,
id: Name::new("x"),
ctx: ast::ExprContext::Load,
});
let not_present = &ast::Expr::FString(ast::ExprFString {
node_index: ast::AtomicNodeIndex::NONE,
range,
value: ast::FStringValue::single(ast::FString {
node_index: ast::AtomicNodeIndex::NONE,
range,
elements: vec![ast::InterpolatedStringElement::Interpolation(
ast::InterpolatedElement {
node_index: ast::AtomicNodeIndex::NONE,
range,
expression: Box::new(expr_x),
debug_text: None,
conversion: ast::ConversionFlag::None,
format_spec: None,
runtime_str: None,
runtime_interpolation_format_spec: None,
runtime_formatted_value_format_spec: None,
},
)]
.into(),
flags,
}),
runtime_joined_str: None,
runtime_values: None,
});
assert!(!Compiler::contains_await(not_present));
// f'{await x}'
let expr_await_x = ast::Expr::Await(ast::ExprAwait {
node_index: ast::AtomicNodeIndex::NONE,
range,
value: Box::new(ast::Expr::Name(ast::ExprName {
node_index: ast::AtomicNodeIndex::NONE,
range,
id: Name::new("x"),
ctx: ast::ExprContext::Load,
})),
});
let present = &ast::Expr::FString(ast::ExprFString {
node_index: ast::AtomicNodeIndex::NONE,
range,
value: ast::FStringValue::single(ast::FString {
node_index: ast::AtomicNodeIndex::NONE,
range,
elements: vec![ast::InterpolatedStringElement::Interpolation(
ast::InterpolatedElement {
node_index: ast::AtomicNodeIndex::NONE,
range,
expression: Box::new(expr_await_x),
debug_text: None,
conversion: ast::ConversionFlag::None,
format_spec: None,
runtime_str: None,
runtime_interpolation_format_spec: None,
runtime_formatted_value_format_spec: None,
},
)]
.into(),
flags,
}),
runtime_joined_str: None,
runtime_values: None,
});
assert!(Compiler::contains_await(present));
// f'{x:{await y}}'
let expr_x = ast::Expr::Name(ast::ExprName {
node_index: ast::AtomicNodeIndex::NONE,
range,
id: Name::new("x"),
ctx: ast::ExprContext::Load,
});
let expr_await_y = ast::Expr::Await(ast::ExprAwait {
node_index: ast::AtomicNodeIndex::NONE,
range,
value: Box::new(ast::Expr::Name(ast::ExprName {
node_index: ast::AtomicNodeIndex::NONE,
range,
id: Name::new("y"),
ctx: ast::ExprContext::Load,
})),
});
let present = &ast::Expr::FString(ast::ExprFString {
node_index: ast::AtomicNodeIndex::NONE,
range,
value: ast::FStringValue::single(ast::FString {
node_index: ast::AtomicNodeIndex::NONE,
range,
elements: vec![ast::InterpolatedStringElement::Interpolation(
ast::InterpolatedElement {
node_index: ast::AtomicNodeIndex::NONE,
range,
expression: Box::new(expr_x),
debug_text: None,
conversion: ast::ConversionFlag::None,
format_spec: Some(Box::new(ast::InterpolatedStringFormatSpec {
node_index: ast::AtomicNodeIndex::NONE,
range,
elements: vec![ast::InterpolatedStringElement::Interpolation(
ast::InterpolatedElement {
node_index: ast::AtomicNodeIndex::NONE,
range,
expression: Box::new(expr_await_y),
debug_text: None,
conversion: ast::ConversionFlag::None,
format_spec: None,
runtime_str: None,
runtime_interpolation_format_spec: None,
runtime_formatted_value_format_spec: None,
},
)]
.into(),
})),
runtime_str: None,
runtime_interpolation_format_spec: None,
runtime_formatted_value_format_spec: None,
},
)]
.into(),
flags,
}),
runtime_joined_str: None,
runtime_values: None,
});
assert!(Compiler::contains_await(present));
}
}
#[cfg(test)]
mod tests {
use super::*;
use rustpython_compiler_core::{
SourceFileBuilder,
bytecode::{CO_FAST_ARG_KW, CO_FAST_ARG_POS, CodeUnit, OpArg},
};
fn assert_scope_exit_locations(code: &CodeObject) {
for (instr, (location, _)) in code.instructions.iter().zip(code.locations.iter()) {
if matches!(
instr.op,
Instruction::ReturnValue
| Instruction::RaiseVarargs { .. }
| Instruction::Reraise { .. }
) {
assert!(
location.line.get() > 0,
"scope-exit instruction {instr:?} is missing a line number"
);
}
}
for constant in code.constants.iter() {
if let ConstantData::Code { code } = constant {
assert_scope_exit_locations(code);
}
}
}
fn compile_exec(source: &str) -> CodeObject {
let opts = CompileOpts::default();
compile_exec_with_options(source, opts)
}
fn compile_single(source: &str) -> CodeObject {
let opts = CompileOpts::default();
let source_file = SourceFileBuilder::new("source_path", source).finish();
let parsed = ruff_python_parser::parse(
source_file.source_text(),
ruff_python_parser::Mode::Module.into(),
)
.unwrap()
.into_syntax();
compile_top(parsed, source_file, Mode::Single, opts).unwrap()
}
fn compile_exec_optimized(source: &str) -> CodeObject {
let opts = CompileOpts {
optimize: 1,
..CompileOpts::default()
};
compile_exec_with_options(source, opts)
}
fn compile_exec_with_options(source: &str, mut opts: CompileOpts) -> CodeObject {
let source_file = SourceFileBuilder::new("source_path", source).finish();
let parsed = ruff_python_parser::parse(
source_file.source_text(),
ruff_python_parser::Mode::Module.into(),
)
.unwrap();
let mut ast = parsed.into_syntax();
opts.future_features |= checked_future_features(&ast, &source_file).unwrap();
let future_annotations = opts
.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false);
let ast = match ast {
ruff_python_ast::Mod::Module(stmts) => stmts,
_ => unreachable!(),
};
let symbol_table = SymbolTable::scan_program_with_options(
&ast,
source_file.clone(),
opts.allow_top_level_await,
opts.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS),
opts.recursion_limit,
)
.map_err(|e| e.into_codegen_error(source_file.name().to_owned()))
.unwrap();
let mut compiler =
Compiler::new_with_syntax_warning_handler(opts, source_file, "<module>", None);
compiler.compile_program(&ast, symbol_table).unwrap();
compiler.exit_scope()
}
fn compile_module_instruction_infos(source: &str, mode: Mode) -> Vec<ir::InstructionInfo> {
let mut opts = CompileOpts::default();
let source_file = SourceFileBuilder::new("source_path", source).finish();
let parsed = ruff_python_parser::parse(
source_file.source_text(),
ruff_python_parser::Mode::Module.into(),
)
.unwrap();
let mut ast = parsed.into_syntax();
opts.future_features |= checked_future_features(&ast, &source_file).unwrap();
let future_annotations = opts
.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
if matches!(mode, Mode::Single)
&& let ruff_python_ast::Mod::Module(module) = &mut ast
{
preprocess::preprocess_statements(
&mut module.body,
opts.optimize,
future_annotations,
false,
);
} else {
preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false);
}
let ast = match ast {
ruff_python_ast::Mod::Module(stmts) => stmts,
_ => unreachable!(),
};
let symbol_table = SymbolTable::scan_program_with_options(
&ast,
source_file.clone(),
opts.allow_top_level_await,
opts.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS),
opts.recursion_limit,
)
.map_err(|e| e.into_codegen_error(source_file.name().to_owned()))
.unwrap();
let mut compiler =
Compiler::new_with_syntax_warning_handler(opts, source_file, "<module>", None);
match mode {
Mode::Single => compiler.compile_program_single(&ast.body, symbol_table),
_ => compiler.compile_program(&ast, symbol_table),
}
.unwrap();
compiler
.current_code_info()
.blocks
.iter()
.flat_map(|block| block.used_instructions().iter().copied())
.collect()
}
fn compile_eval_ast_with_options(expr: ast::Expr, opts: CompileOpts) -> CodeObject {
let source_file = SourceFileBuilder::new("source_path", "").finish();
let parsed = ruff_python_ast::Mod::Expression(ast::ModExpression {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
body: Box::new(expr),
});
compile_top(parsed, source_file, Mode::Eval, opts).unwrap()
}
fn set_ast_constant(expr: &mut ast::Expr, constant: ConstantData) {
let constant = crate::constant_data_to_ast_constant_value(constant);
let range = expr.range();
*expr = ast::Expr::Constant(ast::ExprConstant {
node_index: Default::default(),
range,
value: constant,
kind: None,
invalid_type: None,
});
}
fn compile_ast_constant_expr(mut expr: ast::Expr, constant: ConstantData) -> CodeObject {
set_ast_constant(&mut expr, constant);
compile_eval_ast_with_options(expr, CompileOpts::default())
}
fn first_ast_constant_warning(expr: ast::Expr) -> String {
let opts = CompileOpts::default();
let source_file = SourceFileBuilder::new("source_path", "").finish();
let parsed = ruff_python_ast::Mod::Expression(ast::ModExpression {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
body: Box::new(expr),
});
let mut warning = None;
let mut handler = |location, message: String| {
warning = Some(message.clone());
Err(CodegenError {
location: Some(location),
end_location: None,
error: CodegenErrorType::SyntaxError(message),
source_path: "source_path".to_owned(),
})
};
compile_top_with_syntax_warning_handler(
parsed,
source_file,
Mode::Eval,
opts,
Some(&mut handler),
)
.expect_err("expected SyntaxWarning handler to stop compilation");
warning.expect("expected warning message")
}
fn first_exec_warning(source: &str) -> String {
let opts = CompileOpts::default();
let source_file = SourceFileBuilder::new("source_path", source).finish();
let parsed = ruff_python_parser::parse(
source_file.source_text(),
ruff_python_parser::Mode::Module.into(),
)
.unwrap()
.into_syntax();
let mut warning = None;
let mut handler = |location, message: String| {
warning = Some(message.clone());
Err(CodegenError {
location: Some(location),
end_location: None,
error: CodegenErrorType::SyntaxError(message),
source_path: "source_path".to_owned(),
})
};
compile_top_with_syntax_warning_handler(
parsed,
source_file,
Mode::Exec,
opts,
Some(&mut handler),
)
.expect_err("expected SyntaxWarning handler to stop compilation");
warning.expect("expected warning message")
}
fn frozenset_call_expr() -> ast::Expr {
ast::Expr::Call(ast::ExprCall {
node_index: ast::AtomicNodeIndex::NONE,
range_start: TextSize::default(),
func: Box::new(ast::Expr::Name(ast::ExprName {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
id: ast::name::Name::new_static("frozenset"),
ctx: ast::ExprContext::Load,
})),
arguments: ast::Arguments {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
args: Box::default(),
keywords: Default::default(),
runtime_args: None,
runtime_bases: None,
},
})
}
fn compile_exec_parsed_error(
source: &str,
parsed: ruff_python_parser::Parsed<ruff_python_ast::Mod>,
) -> CodegenError {
let mut opts = CompileOpts::default();
let source_file = SourceFileBuilder::new("source_path", source).finish();
let mut ast = parsed.into_syntax();
opts.future_features |= match checked_future_features(&ast, &source_file) {
Ok(features) => features,
Err(err) => return err,
};
let future_annotations = opts
.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false);
let ast = match ast {
ruff_python_ast::Mod::Module(stmts) => stmts,
_ => unreachable!(),
};
let symbol_table = match SymbolTable::scan_program(&ast, source_file.clone()) {
Ok(symbol_table) => symbol_table,
Err(err) => return err.into_codegen_error(source_file.name().to_owned()),
};
let mut compiler =
Compiler::new_with_syntax_warning_handler(opts, source_file, "<module>", None);
compiler.compile_program(&ast, symbol_table).unwrap_err()
}
fn compile_exec_error(source: &str) -> CodegenError {
let source_file = SourceFileBuilder::new("source_path", source).finish();
let parsed = ruff_python_parser::parse(
source_file.source_text(),
ruff_python_parser::Mode::Module.into(),
)
.unwrap();
compile_exec_parsed_error(source, parsed)
}
fn compile_exec_error_message(source: &str) -> String {
compile_exec_error(source).error.to_string()
}
fn compile_exec_unchecked_error_message(source: &str) -> String {
let source_file = SourceFileBuilder::new("source_path", source).finish();
let parsed = ruff_python_parser::parse_unchecked(
source_file.source_text(),
ruff_python_parser::Mode::Module.into(),
);
compile_exec_parsed_error(source, parsed).error.to_string()
}
#[test]
fn ast_constant_frozenset_compiles_as_load_const() {
let code = compile_ast_constant_expr(
frozenset_call_expr(),
ConstantData::Frozenset {
elements: vec![ConstantData::Integer {
value: BigInt::from(1u8),
}],
},
);
let ops: Vec<_> = code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.iter()
.any(|op| matches!(op, Instruction::LoadConst { .. })),
"public ast.Constant(frozenset(...)) must use CPython Constant_kind LOAD_CONST path, got {ops:?}"
);
assert!(
!ops.iter().any(|op| matches!(
op,
Instruction::LoadName { .. }
| Instruction::Call { .. }
| Instruction::CallKw { .. }
)),
"public ast.Constant(frozenset(...)) must not compile as a frozenset() call, got {ops:?}"
);
assert!(
code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Frozenset { elements }
if matches!(
elements.as_slice(),
[ConstantData::Integer { value }] if *value == BigInt::from(1u8)
)
)),
"missing frozenset constant in code constants"
);
}
#[test]
fn ast_constant_is_not_scanned_as_lowered_expression() {
let mut expr = frozenset_call_expr();
set_ast_constant(
&mut expr,
ConstantData::Frozenset {
elements: Vec::new(),
},
);
let module = ast::ModExpression {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
body: Box::new(expr),
};
let table = SymbolTable::scan_expr_with_options(
&module,
SourceFileBuilder::new("source_path", "").finish(),
false,
false,
CompileOpts::default().recursion_limit,
)
.unwrap();
assert!(
table.lookup(&"frozenset".into()).is_none(),
"CPython symtable Constant_kind does not visit the lowered frozenset() expression"
);
}
#[test]
fn ast_constant_frozenset_call_warns_like_cpython_constant() {
let mut func = frozenset_call_expr();
set_ast_constant(
&mut func,
ConstantData::Frozenset {
elements: Vec::new(),
},
);
let message = first_ast_constant_warning(ast::Expr::Call(ast::ExprCall {
node_index: ast::AtomicNodeIndex::NONE,
range_start: TextSize::default(),
func: Box::new(func),
arguments: ast::Arguments {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
args: Box::default(),
keywords: Default::default(),
runtime_args: None,
runtime_bases: None,
},
}));
assert!(
message.contains("'frozenset' object is not callable"),
"expected public ast.Constant(frozenset()) callable warning, got {message:?}"
);
}
#[test]
fn ast_constant_frozenset_subscript_warns_like_cpython_constant() {
let mut value = frozenset_call_expr();
set_ast_constant(
&mut value,
ConstantData::Frozenset {
elements: Vec::new(),
},
);
let message = first_ast_constant_warning(ast::Expr::Subscript(ast::ExprSubscript {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
value: Box::new(value),
slice: Box::new(ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
value: ast::Number::Int(ast::Int::ZERO),
})),
ctx: ast::ExprContext::Load,
}));
assert!(
message.contains("'frozenset' object is not subscriptable"),
"expected public ast.Constant(frozenset()) subscript warning, got {message:?}"
);
}
#[test]
fn ast_constant_str_bad_index_warns_like_cpython_constant() {
let mut value = frozenset_call_expr();
set_ast_constant(
&mut value,
ConstantData::Str {
value: "abc".into(),
},
);
let message = first_ast_constant_warning(ast::Expr::Subscript(ast::ExprSubscript {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
value: Box::new(value),
slice: Box::new(ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
value: ast::Number::Float(1.0),
})),
ctx: ast::ExprContext::Load,
}));
assert!(
message.contains("str indices must be integers or slices, not float"),
"expected public ast.Constant(str) bad-index warning, got {message:?}"
);
}
#[test]
fn ast_constant_frozenset_is_warns_like_cpython_constant() {
let mut left = frozenset_call_expr();
set_ast_constant(
&mut left,
ConstantData::Frozenset {
elements: Vec::new(),
},
);
let message = first_ast_constant_warning(ast::Expr::Compare(ast::ExprCompare {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
left: Box::new(left),
ops: Box::new([ast::CmpOp::Is]),
comparators: Box::new([ast::Expr::NoneLiteral(ast::ExprNoneLiteral {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
})]),
runtime_comparators: None,
}));
assert!(
message.contains("\"is\" with 'frozenset' literal"),
"expected public ast.Constant(frozenset()) identity warning, got {message:?}"
);
}
#[test]
fn ast_constant_tuple_compiles_as_load_const() {
let expr = ast::Expr::Tuple(ast::ExprTuple {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
elts: Vec::new(),
ctx: ast::ExprContext::Load,
parenthesized: true,
runtime_elts: None,
});
let code = compile_ast_constant_expr(
expr,
ConstantData::Tuple {
elements: vec![ConstantData::Integer {
value: BigInt::from(1u8),
}],
},
);
let ops: Vec<_> = code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.iter()
.any(|op| matches!(op, Instruction::LoadConst { .. })),
"public ast.Constant(tuple(...)) must use CPython Constant_kind LOAD_CONST path, got {ops:?}"
);
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::BuildTuple { .. })),
"public ast.Constant(tuple(...)) must not compile as a tuple display, got {ops:?}"
);
assert!(
code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if matches!(
elements.as_slice(),
[ConstantData::Integer { value }] if *value == BigInt::from(1u8)
)
)),
"missing tuple constant in code constants"
);
}
#[test]
fn ast_constant_slice_bound_uses_cpython_constant_slice_path() {
let mut lower = frozenset_call_expr();
set_ast_constant(
&mut lower,
ConstantData::Integer {
value: BigInt::from(1u8),
},
);
let expr = ast::Expr::Subscript(ast::ExprSubscript {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
value: Box::new(ast::Expr::Name(ast::ExprName {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
id: ast::name::Name::new_static("obj"),
ctx: ast::ExprContext::Load,
})),
slice: Box::new(ast::Expr::Slice(ast::ExprSlice {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
lower: Some(Box::new(lower)),
upper: Some(Box::new(ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
node_index: ast::AtomicNodeIndex::NONE,
range: TextRange::default(),
value: ast::Number::Int(ast::Int::ZERO),
}))),
step: None,
})),
ctx: ast::ExprContext::Load,
});
let code = compile_eval_ast_with_options(expr, CompileOpts::default());
let ops: Vec<_> = code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.iter().any(|op| matches!(
op,
Instruction::BinarySlice | Instruction::BuildSlice { .. }
)),
"public ast.Constant slice bound must follow CPython Constant_kind folded slice path, got {ops:?}"
);
assert!(
code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Slice { elements }
if matches!(
elements.as_ref(),
[
ConstantData::Integer { value },
ConstantData::Integer { .. },
ConstantData::None,
] if *value == BigInt::from(1u8)
)
)),
"missing folded slice constant for public ast.Constant bound"
);
}
#[test]
fn match_pattern_errors_use_cpython_sequence_messages() {
let many_names = (0..256)
.map(|i| format!("a{i}"))
.collect::<Vec<_>>()
.join(", ");
let too_many = format!(
"\
match x:
case [{many_names}, *rest]:
pass
"
);
assert_eq!(
compile_exec_error_message(&too_many),
"too many expressions in star-unpacking sequence pattern"
);
assert_eq!(
compile_exec_error_message(
"\
match x:
case [*a, *b]:
pass
"
),
"multiple starred names in sequence pattern"
);
assert_eq!(
compile_exec_unchecked_error_message(
"\
match x:
case {**_}:
pass
"
),
"invalid syntax"
);
}
#[test]
fn match_mapping_duplicate_literal_keys_use_cpython_equality() {
for (source, expected) in [
(
"\
match x:
case {1: a, True: b}:
pass
",
"mapping pattern checks duplicate key (True)",
),
(
"\
match x:
case {1: a, 1.0: b}:
pass
",
"mapping pattern checks duplicate key (1.0)",
),
(
"\
match x:
case {0.0: a, -0.0: b}:
pass
",
"mapping pattern checks duplicate key (-0.0)",
),
(
"\
match x:
case {9007199254740992: a, 9007199254740992.0: b}:
pass
",
"mapping pattern checks duplicate key (9007199254740992.0)",
),
(
"\
match x:
case {-9007199254740992: a, -9007199254740992.0: b}:
pass
",
"mapping pattern checks duplicate key (-9007199254740992.0)",
),
(
"\
match x:
case {1 + 0j: a, 1: b}:
pass
",
"mapping pattern checks duplicate key (1)",
),
(
"\
match x:
case {1: a, 1 + 0j: b}:
pass
",
"mapping pattern checks duplicate key ((1+0j))",
),
(
"\
match x:
case {0j: a, -0.0: b}:
pass
",
"mapping pattern checks duplicate key (-0.0)",
),
] {
assert_eq!(compile_exec_error_message(source), expected);
}
}
#[test]
fn match_mapping_accepts_folded_literal_keys_like_cpython() {
compile_exec(
"\
def f(x):
match x:
case {-1: a, 1 + 0j: b}:
return a, b
case {9007199254740993: a, 9007199254740992.0: b}:
return a, b
case {-9007199254740993: a, -9007199254740992.0: b}:
return a, b
case {1 + 1j: a, 1: b}:
return a, b
case _:
return None
",
);
}
#[test]
fn match_mapping_accepts_public_ast_constant_keys_like_cpython() {
let source = "\
match x:
case {'a': _, b'b': _, 2: _, 1.5: _, 1j: _, True: _, None: _}:
pass
";
let source_file = SourceFileBuilder::new("source_path", source).finish();
let parsed = ruff_python_parser::parse(
source_file.source_text(),
ruff_python_parser::Mode::Module.into(),
)
.unwrap();
let mut ast = parsed.into_syntax();
let ast::Mod::Module(module) = &mut ast else {
unreachable!();
};
let ast::Stmt::Match(match_stmt) = &mut module.body[0] else {
unreachable!();
};
let ast::Pattern::MatchMapping(mapping) = &mut match_stmt.cases[0].pattern else {
unreachable!();
};
for (key, value) in mapping.keys.iter_mut().zip([
ast::ConstantValue::Str("a".into()),
ast::ConstantValue::Bytes(vec![b'b'].into_boxed_slice()),
ast::ConstantValue::Integer("2".into()),
ast::ConstantValue::Float(1.5),
ast::ConstantValue::Complex {
real: 0.0,
imag: 1.0,
},
ast::ConstantValue::Boolean(true),
ast::ConstantValue::None,
]) {
let range = key.range();
*key = ast::Expr::Constant(ast::ExprConstant {
node_index: ast::AtomicNodeIndex::NONE,
range,
value,
kind: None,
invalid_type: None,
});
}
compile_top(ast, source_file, Mode::Exec, CompileOpts::default()).unwrap();
}
#[test]
fn match_literal_binop_folding_uses_cpython_complex_shape() {
assert!(
Compiler::try_fold_match_pattern_binop(
ast::Operator::Add,
&ConstantData::Integer {
value: BigInt::from(1)
},
&ConstantData::Integer {
value: BigInt::from(2)
},
)
.is_none()
);
assert!(
Compiler::try_fold_match_pattern_binop(
ast::Operator::Add,
&ConstantData::Float { value: 1.0 },
&ConstantData::Float { value: 2.0 },
)
.is_none()
);
assert!(matches!(
Compiler::try_fold_match_pattern_binop(
ast::Operator::Add,
&ConstantData::Integer {
value: BigInt::from(1)
},
&ConstantData::Complex {
value: Complex::new(0.0, 2.0)
},
),
Some(ConstantData::Complex { value }) if value == Complex::new(1.0, 2.0)
));
assert!(matches!(
Compiler::try_fold_match_pattern_binop(
ast::Operator::Sub,
&ConstantData::Float { value: 1.5 },
&ConstantData::Complex {
value: Complex::new(0.0, 2.0)
},
),
Some(ConstantData::Complex { value }) if value == Complex::new(1.5, -2.0)
));
}
#[test]
fn match_literal_patterns_reject_unexpected_constants_like_cpython() {
assert!(Compiler::is_unexpected_match_literal_constant(
&ast::ExprEllipsisLiteral {
range: TextRange::default(),
node_index: ast::AtomicNodeIndex::NONE,
}
.into()
));
}
#[test]
fn unpack_ex_allows_large_after_count_like_cpython() {
let suffix = (0..256)
.map(|i| format!("a{i}"))
.collect::<Vec<_>>()
.join(", ");
let code = compile_exec(&format!(
"\
def assignment(values):
*rest, {suffix} = values
return a255
def pattern(values):
match values:
case [*rest, {suffix}]:
return a255
case _:
return None
"
));
let assignment = find_code(&code, "assignment").expect("missing assignment code");
assert_eq!(
full_opargs_for(assignment, |op| matches!(op, Instruction::UnpackEx { .. })),
vec![256 << 8]
);
let pattern = find_code(&code, "pattern").expect("missing pattern code");
assert_eq!(
full_opargs_for(pattern, |op| matches!(op, Instruction::UnpackEx { .. })),
vec![256 << 8]
);
}
#[test]
fn match_irrefutable_pattern_errors_use_cpython_messages() {
assert_eq!(
compile_exec_error_message(
"\
match x:
case y | 1:
pass
"
),
"name capture 'y' makes remaining patterns unreachable"
);
assert_eq!(
compile_exec_error_message(
"\
match x:
case _ | 1:
pass
"
),
"wildcard makes remaining patterns unreachable"
);
}
#[test]
fn empty_module_implicit_return_inherits_resume_location_like_cpython() {
let code = compile_exec("");
// codegen emits the implicit LOAD_CONST/RETURN_VALUE with
// NO_LOCATION, then flowgraph.c::propagate_line_numbers() propagates
// the module RESUME location, whose line is 0.
assert_eq!(code.linetable.as_ref(), &[0xf2, 0x03, 0x01, 0x01, 0x01]);
}
#[test]
fn module_docstring_load_uses_doc_location_like_cpython() {
let code = compile_exec(
"\
\"doc\"
x = 1
",
);
// codegen_body() emits the docstring LOAD_CONST at the
// string expression location, then emits STORE_NAME __doc__ with
// NO_LOCATION.
assert_eq!(
code.linetable.as_ref(),
&[
0xf0, 0x03, 0x01, 0x01, 0x01, 0xd9, 0x00, 0x05, 0xd8, 0x04, 0x05, 0x82, 0x01,
],
);
}
#[test]
fn redundant_nop_location_copies_full_location_like_cpython() {
let code = compile_exec(
"\
def f(x, y, z):
while x:
if y:
pass
elif z:
if y < 0:
return y
if z:
y = y + 1
elif y:
return 1
return -1
",
);
let f = find_code(&code, "f").expect("missing function code");
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xdf, 0x0a, 0x0b, 0xdf, 0x0b, 0x0c, 0xd9, 0x0c, 0x10, 0xdf, 0x0d, 0x0e,
0xd8, 0x0f, 0x10, 0x90, 0x31, 0x8c, 0x75, 0xd8, 0x17, 0x18, 0x90, 0x08, 0xdf, 0x0f,
0x10, 0xd8, 0x14, 0x15, 0x98, 0x01, 0x95, 0x45, 0x92, 0x01, 0xf1, 0x03, 0x00, 0x10,
0x11, 0xe7, 0x0d, 0x0e, 0x89, 0x51, 0xd9, 0x13, 0x14, 0xd8, 0x0b, 0x0d, 0x80, 0x49,
],
"CPython basicblock_remove_redundant_nops() copies the full NOP location into a following no-location jump"
);
}
fn scan_program_symbol_table(source: &str) -> SymbolTable {
let source_file = SourceFileBuilder::new("source_path", source).finish();
let parsed = ruff_python_parser::parse(
source_file.source_text(),
ruff_python_parser::Mode::Module.into(),
)
.unwrap();
let ast = parsed.into_syntax();
let ast = match ast {
ruff_python_ast::Mod::Module(stmts) => stmts,
_ => unreachable!(),
};
SymbolTable::scan_program(&ast, source_file)
.map_err(|e| e.into_codegen_error("source_path".to_owned()))
.unwrap()
}
fn find_symbol_table<'a>(table: &'a SymbolTable, name: &str) -> Option<&'a SymbolTable> {
if table.name == name {
return Some(table);
}
table
.sub_tables
.iter()
.find_map(|sub_table| find_symbol_table(sub_table, name))
}
fn compile_exec_late_cfg_trace(source: &str) -> Vec<(String, String)> {
let opts = CompileOpts::default();
let source_file = SourceFileBuilder::new("source_path", source).finish();
let parsed = ruff_python_parser::parse(
source_file.source_text(),
ruff_python_parser::Mode::Module.into(),
)
.unwrap();
let ast = parsed.into_syntax();
let ast = match ast {
ruff_python_ast::Mod::Module(stmts) => stmts,
_ => unreachable!(),
};
let symbol_table = SymbolTable::scan_program(&ast, source_file.clone())
.map_err(|e| e.into_codegen_error(source_file.name().to_owned()))
.unwrap();
let mut compiler =
Compiler::new_with_syntax_warning_handler(opts, source_file, "<module>", None);
compiler.compile_program(&ast, symbol_table).unwrap();
compiler.pop_symbol_table();
let stack_top = compiler.code_stack.pop().unwrap();
stack_top.debug_late_cfg_trace().unwrap()
}
fn compile_single_function_late_cfg_trace(
source: &str,
function_name: &str,
) -> Vec<(String, String)> {
let opts = CompileOpts::default();
let source_file = SourceFileBuilder::new("source_path", source).finish();
let parsed = ruff_python_parser::parse(
source_file.source_text(),
ruff_python_parser::Mode::Module.into(),
)
.unwrap();
let ast = parsed.into_syntax();
let ast = match ast {
ruff_python_ast::Mod::Module(stmts) => stmts,
_ => unreachable!(),
};
let mut symbol_table = SymbolTable::scan_program(&ast, source_file.clone())
.map_err(|e| e.into_codegen_error(source_file.name().to_owned()))
.unwrap();
let function = ast
.body
.iter()
.find_map(|stmt| match stmt {
ast::Stmt::FunctionDef(f) if f.name.as_str() == function_name => Some(f),
_ => None,
})
.unwrap_or_else(|| panic!("missing function {function_name}"));
symbol_table.next_sub_table = symbol_table
.sub_tables
.iter()
.position(|table| table.name == function_name)
.unwrap_or_else(|| panic!("missing symbol table for {function_name}"));
let name = &function.name;
let parameters = &function.parameters;
let body = &function.body;
let is_async = function.is_async;
let range = function.range();
let mut compiler =
Compiler::new_with_syntax_warning_handler(opts, source_file, "<module>", None);
compiler.future_annotations = symbol_table.future_annotations;
compiler.symbol_table_stack.push(symbol_table);
compiler.set_source_range(range);
compiler.enter_function(name.as_str(), parameters).unwrap();
compiler
.current_code_info()
.flags
.set(bytecode::CodeFlags::COROUTINE, is_async);
let prev_ctx = compiler.ctx;
compiler.ctx = CompileContext {
in_class: prev_ctx.in_class,
func: if is_async {
FunctionContext::AsyncFunction
} else {
FunctionContext::Function
},
in_async_scope: is_async,
};
compiler.set_qualname();
let (_, body) = split_doc(body, &compiler.opts);
let start_label = compiler.use_cpython_function_start_label();
let is_gen = is_async || compiler.current_symbol_table().is_generator;
let stop_iteration_block = if is_gen {
let handler_block = compiler.new_block();
compiler.insert_cpython_stopiteration_setup_cleanup(handler_block);
compiler
.push_fblock_labels(
FBlockType::StopIteration,
start_label,
ir::InstructionSequenceLabel::NO_LABEL,
FBlockDatum::None,
)
.unwrap();
Some(handler_block)
} else {
None
};
compiler.compile_statements(body).unwrap();
match body.last() {
Some(ast::Stmt::Return(_)) => {}
_ => compiler.emit_return_const_no_location(ConstantData::None),
}
if compiler.current_code_info().metadata.consts.is_empty() {
compiler.arg_constant(ConstantData::None);
}
if let Some(handler_block) = stop_iteration_block {
compiler.pop_fblock_label(FBlockType::StopIteration, start_label);
compiler.use_cpython_label_block(handler_block);
emit!(
compiler,
Instruction::CallIntrinsic1 {
func: oparg::IntrinsicFunction1::StopIterationError
}
);
compiler.set_no_location();
emit!(compiler, Instruction::Reraise { depth: 1u32 });
compiler.set_no_location();
}
compiler.pop_symbol_table();
let stack_top = compiler.code_stack.pop().unwrap();
stack_top.debug_late_cfg_trace().unwrap()
}
#[test]
fn try_else_nested_try_const_list_keeps_setup_finally_nop() {
let trace = compile_single_function_late_cfg_trace(
r#"
def f(arch):
try:
[arch, *_] = g()
except OSError:
pass
else:
try:
arch = ['x86', 'MIPS', 'Alpha', 'PowerPC', None,
'ARM', 'ia64', None, None,
'AMD64', None, None, 'ARM64',
][int(arch)]
except (ValueError, IndexError):
pass
else:
if arch:
return arch
"#,
"f",
);
let (_, dump) = trace
.iter()
.find(|(label, _)| label == "after_convert_pseudo_ops")
.expect("missing convert_pseudo_ops trace");
assert!(
dump.contains("[disp=8:9 raw=8:9-17:28 override=None] Real(Nop)"),
"SETUP_FINALLY should survive as a line-bearing NOP like CPython"
);
assert!(
dump.contains("[disp=9:20 raw=9:20-12:14 override=None] Real(BuildList"),
"CPython optimize_lists_and_sets() restores the literal location to BUILD_LIST"
);
}
#[test]
fn for_try_except_break_keeps_cpython_if_layout() {
let code = compile_exec(
"\
def f(support, func, value):
for _ in support.sleeping_retry(support.SHORT_TIMEOUT):
try:
if func() == value:
break
except NotImplementedError:
break
sink(value)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect::<Vec<_>>();
let cond = ops
.iter()
.position(|op| matches!(op, Instruction::PopJumpIfFalse { .. }))
.expect("missing CPython-style false jump for if/break");
assert!(
matches!(
ops.get(cond..cond + 5),
Some([
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::PopTop,
Instruction::JumpForward { .. },
Instruction::JumpBackward { .. },
])
),
"CPython codegen_if() keeps the break cleanup in the true-body fallthrough before the loop backedge, got ops={ops:?}"
);
}
#[test]
fn try_else_loop_break_keeps_body_before_protected_backedge() {
let code = compile_exec(
"\
def f(input):
while 1:
try:
pass
except IndexError:
break
else:
key = None
while key is None:
key = input()
if key not in ('', 'q'):
key = None
if key == 'q':
break
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect::<Vec<_>>();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadConst { .. },
Instruction::ReturnValue,
Instruction::JumpBackward { .. },
]
)
}),
"CPython codegen_if() keeps the break body before the false backedge into the protected try/except loop, got ops={ops:?}"
);
}
#[test]
fn loop_nested_if_tail_keeps_duplicate_jump_back_label() {
let code = compile_exec(
"\
def f(value, digits):
for digit in value:
if isinstance(digit, int) and 0 <= digit <= 9:
if digits or digit != 0:
digits.append(digit)
else:
raise ValueError('x')
return digits
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache | Instruction::Nop))
.collect::<Vec<_>>();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::JumpBackward { .. },
Instruction::JumpBackward { .. },
Instruction::LoadGlobal { .. },
]
)
}),
"CPython codegen_if() leaves a distinct no-location end label before the loop else/raise path, got ops={ops:?}"
);
}
#[test]
fn match_for_break_threads_empty_end_label_to_outer_backedge() {
let code = compile_exec(
"\
def f(items, T):
for st in items:
match st.type:
case T.TYPE:
for c in st.children:
if c.name == st.name:
x = 1
break
return x
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache | Instruction::Nop))
.collect::<Vec<_>>();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::JumpBackward { .. },
Instruction::EndFor,
Instruction::PopIter,
Instruction::JumpBackward { .. },
]
)
}),
"CPython codegen_break() threads the match-case inner for break through the empty end label to the outer loop backedge, got ops={ops:?}"
);
}
#[test]
fn match_constant_guard_keeps_cpython_guard_nop_before_subject_pop() {
let code = compile_exec(
"\
def f(self):
x = 0
match x:
case 0 if True:
y = 0
case 0 if True:
y = 1
self.assertEqual(x, 0)
self.assertEqual(y, 0)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect::<Vec<_>>();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::Nop,
Instruction::PopTop,
Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. },
Instruction::StoreFast { .. },
]
)
}),
"CPython codegen_match_inner() emits the guard through codegen_jump_if(), and flowgraph.c keeps the folded constant-guard NOP in a separate success block before POP_TOP, got ops={ops:?}"
);
}
#[test]
fn match_or_default_tail_uses_cpython_load_fast_borrow() {
let code = compile_exec(
"\
def f(format, annotationlib, cls, annotation_fields, return_type, MISSING):
Format = annotationlib.Format
match format:
case Format.VALUE | Format.FORWARDREF | Format.STRING:
cls_annotations = {}
for base in reversed(cls.__mro__):
cls_annotations.update(
annotationlib.get_annotations(base, format=format)
)
new_annotations = {}
for k in annotation_fields:
try:
new_annotations[k] = cls_annotations[k]
except KeyError:
pass
if return_type is not MISSING:
if format == Format.STRING:
new_annotations['return'] = annotationlib.type_repr(return_type)
else:
new_annotations['return'] = return_type
return new_annotations
case _:
raise NotImplementedError(format)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let raise = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. }))
.expect("missing default raise");
let load_format = &ops[raise - 2];
let Instruction::LoadFastBorrow { var_num } = load_format.op else {
panic!(
"CPython codegen_match_inner() emits the default case without a load-fast barrier, so optimize_load_fast() borrows the raise argument; got ops={ops:?}"
);
};
let arg = OpArg::new(u32::from(u8::from(load_format.arg)));
assert_eq!(f.varnames[usize::from(var_num.get(arg))], "format");
}
#[test]
fn preceding_match_or_default_tail_keeps_cpython_strong_load_fast() {
let code = compile_exec(
"\
def f(format):
match format:
case _lazy_annotationlib.Format.VALUE | _lazy_annotationlib.Format.FORWARDREF:
return checked_types
case _lazy_annotationlib.Format.STRING:
return _lazy_annotationlib.annotations_to_string(types)
case _:
raise NotImplementedError(format)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let raise = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. }))
.expect("missing default raise");
let load_format = &ops[raise - 2];
let Instruction::LoadFast { var_num } = load_format.op else {
panic!(
"CPython keeps the default raise argument strong when an earlier copied OR-pattern precedes the final non-default case; got ops={ops:?}"
);
};
let arg = OpArg::new(u32::from(u8::from(load_format.arg)));
assert_eq!(f.varnames[usize::from(var_num.get(arg))], "format");
}
#[test]
fn try_else_after_nested_try_except_exit_keeps_cpython_strong_load_fast() {
let code = compile_exec(
"\
def f(self):
try:
try:
1 / 0
except ZeroDivisionError:
raise OSError
except OSError as e:
self.assertIsInstance(e.__context__, ZeroDivisionError)
else:
self.fail('No exception raised')
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let fail = ops
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "fail"
}
_ => false,
})
.expect("missing fail load");
assert!(
matches!(ops[fail - 1].op, Instruction::LoadFast { .. }),
"CPython codegen_try_except() keeps the orelse entry after a nested try/except end label strong, got ops={ops:?}"
);
}
#[test]
fn try_after_inherited_try_barrier_keeps_successor_loads_strong() {
let code = compile_exec(
"\
def f(self, x):
try:
1 / 0
except EOFError:
pass
except TypeError as msg:
pass
except:
pass
else:
pass
try:
x
except (EOFError, TypeError, ZeroDivisionError):
pass
with self.assertRaises(SyntaxError):
pass
def g(self):
try:
1 / 0
except:
pass
try:
1 / 0
except (EOFError, TypeError, ZeroDivisionError):
pass
with self.assertRaises(SyntaxError):
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let x_loads = load_fast_ops_for_var(f, "x");
assert!(
x_loads
.iter()
.all(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython codegen_try_except() reaches the first try end through USE_LABEL(end); flowgraph.c keeps that barrier state through the following try end, got x loads {x_loads:?}"
);
let self_loads = load_fast_ops_for_var(f, "self");
assert!(
self_loads
.iter()
.all(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython keeps successor with-statement loads strong after a try that started from an inherited try barrier, got self loads {self_loads:?}"
);
let g = find_code(&code, "g").expect("missing g code");
let g_self_loads = load_fast_ops_for_var(g, "self");
assert!(
g_self_loads
.iter()
.all(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython keeps a bare-handler try end label as a barrier when the next statement is another try, got self loads {g_self_loads:?}"
);
}
#[test]
fn loop_continue_try_before_try_else_keeps_orelse_loads_strong() {
let code = compile_exec(
"\
def f(candidate_locales, locales):
for loc in candidate_locales:
try:
work(loc)
except Error:
continue
encoding = getencoding()
try:
localeconv()
except Exception as err:
print(loc, encoding, type(err), err)
else:
locales.append(loc)
",
);
let f = find_code(&code, "f").expect("missing f code");
let locales_loads = load_fast_ops_for_var(f, "locales");
assert!(
locales_loads
.iter()
.all(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython codegen_try_except() leaves an empty end label after a loop try whose handlers continue; optimize_load_fast() keeps the following try/else append receiver strong, got {locales_loads:?}"
);
let loc_loads = load_fast_ops_for_var(f, "loc");
assert!(
loc_loads
.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython keeps the try/else append argument strong after the inherited end-label barrier, got {loc_loads:?}"
);
}
#[test]
fn try_else_after_try_finally_conditional_finalbody_keeps_store_attr_loads_strong() {
let code = compile_exec(
"\
def f(self, w, pid, prev):
try:
try:
if cond:
prev = call()
pid = spawn()
finally:
if prev is not None:
reset(prev)
except:
close(w)
raise
else:
self._fd = w
self._pid = pid
finally:
close(r)
",
);
let f = find_code(&code, "f").expect("missing f code");
let w_self_pairs = load_fast_pair_ops_for_vars(f, "w", "self");
let pid_self_pairs = load_fast_pair_ops_for_vars(f, "pid", "self");
assert!(
w_self_pairs
.iter()
.all(|op| matches!(op, Instruction::LoadFastLoadFast { .. }))
&& !w_self_pairs.is_empty(),
"CPython codegen_try_finally() calls codegen_try_except() inside the active finally try; the else suite starts from the inner try/finally exit label and keeps w/self strong, got {w_self_pairs:?}"
);
assert!(
pid_self_pairs
.iter()
.all(|op| matches!(op, Instruction::LoadFastLoadFast { .. }))
&& !pid_self_pairs.is_empty(),
"CPython keeps the second store-attr source pair strong in the same try/except/else/finally else suite, got {pid_self_pairs:?}"
);
}
#[test]
fn try_except_end_before_following_try_keeps_protected_attr_loads_strong() {
let code = compile_exec(
"\
def f(f, dotlock=True):
dotlock_done = False
try:
if dotlock:
try:
pre_lock = _create_temporary(f.name + '.lock')
pre_lock.close()
except OSError as e:
if e.errno in (errno.EACCES, errno.EROFS):
return
else:
raise
try:
try:
os.link(pre_lock.name, f.name + '.lock')
dotlock_done = True
except (AttributeError, PermissionError):
os.rename(pre_lock.name, f.name + '.lock')
dotlock_done = True
else:
os.unlink(pre_lock.name)
except FileExistsError:
os.remove(pre_lock.name)
raise ExternalClashError('dot lock unavailable: %s' %
f.name)
except:
if dotlock_done:
os.remove(f.name + '.lock')
raise
",
);
let f = find_code(&code, "f").expect("missing f code");
let pre_lock_loads = load_fast_ops_for_var(f, "pre_lock");
let strong_pre_lock_loads = pre_lock_loads
.iter()
.filter(|op| matches!(op, Instruction::LoadFast { .. }))
.count();
assert!(
strong_pre_lock_loads >= 2,
"CPython codegen_try_except() emits USE_LABEL(end) before the following try; flowgraph.c::optimize_load_fast() does not push fallthrough through the empty end label, so protected pre_lock attr receivers stay strong, got {pre_lock_loads:?}"
);
let f_loads = load_fast_ops_for_var(f, "f");
assert!(
f_loads
.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython keeps the f.name receiver inside the following protected try strong after the preceding try/except end label, got {f_loads:?}"
);
}
#[test]
fn try_except_method_probe_end_before_if_keeps_loads_strong() {
let code = compile_exec(
"\
def f(param, value=None, quote=True):
if value is not None and len(value) > 0:
if isinstance(value, tuple):
param += '*'
value = encode(value[2], value[0], value[1])
return f'{param}={value}'
else:
try:
value.encode('ascii')
except UnicodeEncodeError:
param += '*'
value = encode(value, 'utf-8', '')
return f'{param}={value}'
if quote or tspecials.search(value):
return f'{param}=\"{quote_value(value)}\"'
else:
return f'{param}={value}'
else:
return param
",
);
let f = find_code(&code, "f").expect("missing f code");
let quote_loads = load_fast_ops_for_var(f, "quote");
assert!(
quote_loads
.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython codegen_try_except() emits USE_LABEL(end) after a handled method probe; flowgraph.c::optimize_load_fast() leaves the following if-test local strong, got {quote_loads:?}"
);
let param_loads = load_fast_ops_for_var(f, "param");
assert!(
param_loads
.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython keeps the return f-string local loads strong after the protected method-probe end label, got {param_loads:?}"
);
let value_loads = load_fast_ops_for_var(f, "value");
assert!(
value_loads
.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython keeps the post-try value loads strong after the protected method-probe end label, got {value_loads:?}"
);
}
#[test]
fn try_except_fallthrough_before_return_call_keeps_borrow() {
let code = compile_exec(
"\
def f(obj, lock, ctx, cls, class_cache):
if cond1(obj):
return Synchronized(obj, lock, ctx)
elif cond2(obj):
return SynchronizedArray(obj, lock, ctx)
else:
try:
scls = class_cache[cls]
except KeyError:
scls = make_synchronized(cls)
return scls(obj, lock, ctx)
",
);
let f = find_code(&code, "f").expect("missing f code");
let scls_loads = load_fast_ops_for_var(f, "scls");
assert!(
scls_loads
.iter()
.all(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"CPython codegen_try_except() emits USE_LABEL(end) and the following return call into the same basic block; flowgraph.c borrows the callable local, got {scls_loads:?}"
);
let obj_lock_pairs = load_fast_pair_ops_for_vars(f, "obj", "lock");
assert!(
obj_lock_pairs
.iter()
.all(|op| matches!(op, Instruction::LoadFastBorrowLoadFastBorrow { .. })),
"CPython flowgraph.c borrows the return call argument pair after a typed fallthrough handler, got {obj_lock_pairs:?}"
);
let ctx_loads = load_fast_ops_for_var(f, "ctx");
assert!(
ctx_loads
.iter()
.all(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"CPython flowgraph.c borrows the trailing return call argument after the try-end label, got {ctx_loads:?}"
);
}
#[test]
fn try_except_comprehension_handler_before_return_call_keeps_borrow() {
let code = compile_exec(
"\
def f(obj, lock, ctx):
assert not isinstance(obj, SynchronizedBase), 'object already synchronized'
ctx = ctx or get_context()
if isinstance(obj, ctypes._SimpleCData):
return Synchronized(obj, lock, ctx)
elif isinstance(obj, ctypes.Array):
if obj._type_ is ctypes.c_char:
return SynchronizedString(obj, lock, ctx)
return SynchronizedArray(obj, lock, ctx)
else:
cls = type(obj)
try:
scls = class_cache[cls]
except KeyError:
names = [field[0] for field in cls._fields_]
d = {name: make_property(name) for name in names}
classname = 'Synchronized' + cls.__name__
scls = class_cache[cls] = type(classname, (SynchronizedBase,), d)
return scls(obj, lock, ctx)
",
);
let f = find_code(&code, "f").expect("missing f code");
let scls_loads = load_fast_ops_for_var(f, "scls");
assert!(
scls_loads
.iter()
.any(|op| matches!(op, Instruction::LoadFastBorrow { .. }))
&& scls_loads
.iter()
.all(|op| !matches!(op, Instruction::LoadFast { .. })),
"CPython codegen_try_except() keeps the return call in the end-label block even when the handler contains comprehensions, got {scls_loads:?}"
);
let obj_lock_pairs = load_fast_pair_ops_for_vars(f, "obj", "lock");
assert!(
obj_lock_pairs
.iter()
.any(|op| matches!(op, Instruction::LoadFastBorrowLoadFastBorrow { .. }))
&& obj_lock_pairs
.iter()
.all(|op| !matches!(op, Instruction::LoadFastLoadFast { .. })),
"CPython flowgraph.c borrows the return call argument pair after the try-end label and cold handler reordering, got {obj_lock_pairs:?}"
);
let instructions = non_cache_instructions(f).collect::<Vec<_>>();
let ctx_idx = varname_index(f, "ctx");
let has_borrowed_return_tail = instructions.windows(6).any(|window| {
matches!(window[0].op, Instruction::LoadFastBorrow { .. })
&& matches!(window[1].op, Instruction::PushNull)
&& matches!(
window[2].op,
Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
&& matches!(window[3].op, Instruction::LoadFastBorrow { .. })
&& {
let Instruction::LoadFastBorrow { var_num } = window[3].op else {
return false;
};
usize::from(var_num.get(OpArg::new(u32::from(u8::from(window[3].arg)))))
== ctx_idx
}
&& matches!(window[4].op, Instruction::Call { .. })
&& matches!(window[5].op, Instruction::ReturnValue)
});
assert!(
has_borrowed_return_tail,
"CPython flowgraph.c borrows the full final return-call tail after the protected try body, got instructions={instructions:?}"
);
}
#[test]
fn try_finally_closed_conditional_exit_allows_cpython_borrow() {
let code = compile_exec(
"\
def f(self, os, tempfile, oldmode):
try:
work()
finally:
if os.name == 'nt':
os.chmod(tempfile.tempdir, oldmode)
else:
os.chmod(tempfile.tempdir, oldmode)
self.assertEqual(os.listdir(tempfile.tempdir), [])
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let assert_equal = ops
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "assertEqual"
}
_ => false,
})
.expect("missing assertEqual load");
assert!(
matches!(ops[assert_equal - 1].op, Instruction::LoadFastBorrow { .. }),
"CPython codegen_try_finally() does not make a load-fast barrier after a closed conditional finalbody, got ops={ops:?}"
);
}
#[test]
fn handler_resume_after_nested_try_keeps_successor_load_fast_strong() {
let code = compile_exec(
r#"
def f(x):
try:
try:
import readline
except ImportError:
readline = None
else:
import rlcompleter
except ImportError:
return
try:
if x:
y = 1
except ImportError:
return
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let truthiness_load = ops
.windows(3)
.find(|window| {
let arg = OpArg::new(u32::from(u8::from(window[0].arg)));
matches!(
window[0].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(arg))] == "x"
) && matches!(window[1].op, Instruction::ToBool)
&& matches!(window[2].op, Instruction::PopJumpIfFalse { .. })
})
.unwrap_or_else(|| {
panic!(
"missing if x truthiness load: {:?}",
ops.iter().map(|unit| unit.op).collect::<Vec<_>>()
)
});
assert!(
matches!(truthiness_load[0].op, Instruction::LoadFast { .. }),
"CPython flowgraph.c leaves an empty try-end label before this successor block, so optimize_load_fast() does not borrow the if-test load: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn nested_finally_handler_try_end_keeps_return_load_fast_strong() {
let code = compile_exec(
r#"
def f(sys):
try:
import _testinternalcapi
depth = _testinternalcapi.get_recursion_depth()
except (ImportError, RecursionError) as exc:
try:
depth = 0
frame = sys._getframe()
while frame is not None:
depth += 1
frame = frame.f_back
finally:
frame = None
return max(depth - 1, 1)
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let depth_load = ops
.windows(3)
.find(|window| {
let arg = OpArg::new(u32::from(u8::from(window[0].arg)));
matches!(
window[0].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(arg))] == "depth"
) && matches!(window[1].op, Instruction::LoadSmallInt { .. })
&& matches!(window[2].op, Instruction::BinaryOp { .. })
})
.unwrap_or_else(|| {
panic!(
"missing return depth load: {:?}",
ops.iter().map(|unit| unit.op).collect::<Vec<_>>()
)
});
assert!(
matches!(depth_load[0].op, Instruction::LoadFast { .. }),
"CPython flowgraph.c preserves an empty try-end label before this return block, so optimize_load_fast() leaves depth strong: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn try_except_finally_exit_label_keeps_return_load_fast_strong() {
let code = compile_exec(
r#"
def f():
global _importing_zlib
if _importing_zlib:
_bootstrap._verbose_message('zipimport: zlib UNAVAILABLE')
raise ZipImportError("can't decompress data; zlib not available")
_importing_zlib = True
try:
from zlib import decompress
except Exception:
_bootstrap._verbose_message('zipimport: zlib UNAVAILABLE')
raise ZipImportError("can't decompress data; zlib not available")
finally:
_importing_zlib = False
_bootstrap._verbose_message('zipimport: zlib available')
return decompress
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let return_load = ops
.windows(2)
.find(|window| {
let arg = OpArg::new(u32::from(u8::from(window[0].arg)));
matches!(
window[0].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(arg))] == "decompress"
) && matches!(window[1].op, Instruction::ReturnValue)
})
.unwrap_or_else(|| {
panic!(
"missing return decompress load: {:?}",
ops.iter().map(|unit| unit.op).collect::<Vec<_>>()
)
});
assert!(
matches!(return_load[0].op, Instruction::LoadFast { .. }),
"CPython codegen_try_finally() emits a JUMP_NO_INTERRUPT to an empty exit label after the normal finally body, and flowgraph.c::optimize_load_fast() does not fall through an empty block: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn bare_except_finally_exit_label_keeps_successor_load_fast_strong() {
let code = compile_exec(
r#"
def f(self):
hit = False
try:
pass
except:
hit = True
finally:
done = True
self.assertFalse(hit)
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let assert_false = ops
.windows(3)
.find(|window| {
let self_arg = OpArg::new(u32::from(u8::from(window[0].arg)));
let attr_arg = OpArg::new(u32::from(u8::from(window[1].arg)));
matches!(
window[0].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(self_arg))] == "self"
) && matches!(
window[1].op,
Instruction::LoadAttr { namei }
if f.names[usize::try_from(namei.get(attr_arg).name_idx()).unwrap()]
== "assertFalse"
) && matches!(
window[2].op,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }
)
})
.unwrap_or_else(|| {
panic!(
"missing assertFalse call: {:?}",
ops.iter().map(|unit| unit.op).collect::<Vec<_>>()
)
});
assert!(
matches!(assert_false[0].op, Instruction::LoadFast { .. })
&& matches!(assert_false[2].op, Instruction::LoadFast { .. }),
"CPython codegen_try_finally() wraps codegen_try_except(); with a bare handler, the normal finally body jumps to an empty exit label, and flowgraph.c::optimize_load_fast() does not fall through that empty block: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn typed_except_finally_fallthrough_keeps_successor_load_fast_borrow() {
let code = compile_exec(
r#"
def f(self):
hit = False
try:
pass
except Exception:
hit = True
finally:
done = True
self.assertFalse(hit)
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let assert_false = ops
.windows(3)
.find(|window| {
let self_arg = OpArg::new(u32::from(u8::from(window[0].arg)));
let attr_arg = OpArg::new(u32::from(u8::from(window[1].arg)));
matches!(
window[0].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(self_arg))] == "self"
) && matches!(
window[1].op,
Instruction::LoadAttr { namei }
if f.names[usize::try_from(namei.get(attr_arg).name_idx()).unwrap()]
== "assertFalse"
) && matches!(
window[2].op,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }
)
})
.unwrap_or_else(|| {
panic!(
"missing assertFalse call: {:?}",
ops.iter().map(|unit| unit.op).collect::<Vec<_>>()
)
});
assert!(
matches!(assert_false[0].op, Instruction::LoadFastBorrow { .. })
&& matches!(assert_false[2].op, Instruction::LoadFastBorrow { .. }),
"CPython typed-handler fallthrough keeps this successor reachable for optimize_load_fast(), so the loads remain borrowed: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn bare_except_finally_no_exception_shares_return_target() {
let code = compile_exec(
"\
def func():
try:
2
except:
4
finally:
6
",
);
let f = find_code(&code, "func").expect("missing func code");
let ops = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect::<Vec<_>>();
let first_push_exc = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing PushExcInfo");
let returns_before_handler = ops[..first_push_exc]
.iter()
.filter(|op| matches!(op, Instruction::ReturnValue))
.count();
assert_eq!(
returns_before_handler, 1,
"CPython codegen_try_finally() wraps codegen_try_except(); the bare handler jumps back to the normal finally return target instead of forcing duplicate_end_returns() to create a Rust-only return copy, got ops={ops:?}"
);
}
#[test]
fn for_exhaustion_assert_false_message_borrows_load_fast() {
let code = compile_exec(
r#"
def f(arg, opcode):
for i, nb_op in enumerate(opcode._nb_ops):
if arg == nb_op[0]:
return i
assert False, f"{arg} is not a valid BINARY_OP argument."
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let assertion_arg_load = ops
.windows(2)
.find(|window| {
let arg = OpArg::new(u32::from(u8::from(window[0].arg)));
matches!(
window[0].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(arg))] == "arg"
) && matches!(window[1].op, Instruction::FormatSimple)
})
.unwrap_or_else(|| {
panic!(
"missing assertion message arg load: {:?}",
ops.iter().map(|unit| unit.op).collect::<Vec<_>>()
)
});
assert!(
matches!(assertion_arg_load[0].op, Instruction::LoadFastBorrow { .. }),
"CPython codegen_assert() emits AssertionError directly after the failing test, so flowgraph.c::optimize_load_fast() visits the assertion message block: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn try_except_else_conditional_join_borrows_else_receiver() {
let code = compile_exec(
r#"
def f(self):
try:
if not self.result_is_file() or not self.sendfile():
for data in self.result:
self.write(data)
self.finish_content()
except:
if hasattr(self.result, 'close'):
self.result.close()
raise
else:
self.close()
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let else_close_load = ops
.windows(6)
.find(|window| {
let load_arg = OpArg::new(u32::from(u8::from(window[0].arg)));
let attr_arg = OpArg::new(u32::from(u8::from(window[1].arg)));
matches!(
window[0].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(load_arg))] == "self"
) && matches!(
window[1].op,
Instruction::LoadAttr { namei }
if f.names[usize::try_from(namei.get(attr_arg).name_idx()).unwrap()]
== "close"
) && matches!(window[2].op, Instruction::Call { .. })
&& matches!(window[3].op, Instruction::PopTop)
&& matches!(window[4].op, Instruction::LoadConst { .. })
&& matches!(window[5].op, Instruction::ReturnValue)
})
.unwrap_or_else(|| {
panic!(
"missing else self.close return sequence: {:?}",
ops.iter().map(|unit| unit.op).collect::<Vec<_>>()
)
});
assert!(
matches!(else_close_load[0].op, Instruction::LoadFastBorrow { .. }),
"CPython codegen_try_except() emits Try.orelse directly with VISIT_SEQ(), so flowgraph.c::optimize_load_fast() reaches the else self.close receiver: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn try_finally_exit_label_reuses_empty_block_for_borrow() {
let code = compile_exec(
r#"
def f(self, exc):
try:
end_time = self.get_time()
finally:
result = self.context.__exit__(*exc)
self.seconds = end_time - self.start_time
return result
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
let seconds_store = ops
.windows(5)
.find(|window| {
let load_pair = OpArg::new(u32::from(u8::from(window[0].arg)));
let self_load = OpArg::new(u32::from(u8::from(window[3].arg)));
matches!(
window[0].op,
Instruction::LoadFastLoadFast { var_nums }
| Instruction::LoadFastBorrowLoadFastBorrow { var_nums }
if {
let (left, right) = var_nums.get(load_pair).indexes();
f.varnames[usize::from(left)] == "end_time"
&& f.varnames[usize::from(right)] == "self"
}
) && matches!(window[1].op, Instruction::LoadAttr { .. })
&& matches!(window[2].op, Instruction::BinaryOp { .. })
&& matches!(
window[3].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(self_load))] == "self"
)
&& matches!(window[4].op, Instruction::StoreAttr { .. })
})
.unwrap_or_else(|| {
panic!(
"missing self.seconds store sequence: {:?}",
ops.iter().map(|unit| unit.op).collect::<Vec<_>>()
)
});
let return_result = ops
.windows(2)
.find(|window| {
let arg = OpArg::new(u32::from(u8::from(window[0].arg)));
matches!(
window[0].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(arg))] == "result"
) && matches!(window[1].op, Instruction::ReturnValue)
})
.unwrap_or_else(|| {
panic!(
"missing result return sequence: {:?}",
ops.iter().map(|unit| unit.op).collect::<Vec<_>>()
)
});
assert!(
matches!(
seconds_store[0].op,
Instruction::LoadFastBorrowLoadFastBorrow { .. }
) && matches!(seconds_store[3].op, Instruction::LoadFastBorrow { .. })
&& matches!(return_result[0].op, Instruction::LoadFastBorrow { .. }),
"CPython codegen_try_finally() emits JUMP_NO_INTERRUPT to the exit label, and flowgraph.c labels the current empty block instead of inserting a b_next barrier before following code: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn with_tail_while_true_break_successor_uses_strong_load() {
let code = compile_exec(
r#"
def f(self, cm):
with cm as out:
while 1:
data = out.read()
if not data:
break
self.close()
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let close_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "close"
}
_ => false,
})
.expect("missing close load");
assert!(
matches!(
instructions[close_attr - 1].op,
Instruction::LoadFast { .. }
),
"CPython codegen_while() emits USE_LABEL(end) for the tail break target before codegen_with_inner() emits normal __exit__ cleanup, so flowgraph.c::optimize_load_fast() leaves the successor receiver strong: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn folded_ifexp_nested_in_call_keeps_successor_load_fast_strong() {
let code = compile_exec(
r#"
def f(self, g):
self.x = g(self.y, optimization='' if __debug__ else 1)
self.close()
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let close_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "close"
}
_ => false,
})
.expect("missing close load");
assert!(
matches!(
instructions[close_attr - 1].op,
Instruction::LoadFast { .. }
),
"CPython codegen_ifexp() emits USE_LABEL(end); with a folded conditional nested in a larger stack expression, flowgraph.c::optimize_load_fast() sees an empty end block and does not visit the successor loads: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn folded_ifexp_assignment_before_with_keeps_context_load_fast_strong() {
let code = compile_exec(
r#"
def f(self, cm):
optlevel = 1 if __debug__ else 0
with cm as t:
self.use(t, optlevel)
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let first_exit = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadSpecial { method } => {
method.get(OpArg::new(u32::from(u8::from(unit.arg)))) == SpecialMethod::Exit
}
_ => false,
})
.expect("missing __exit__ load");
let cm_load = (0..first_exit)
.rev()
.find(|idx| {
let arg = OpArg::new(u32::from(u8::from(instructions[*idx].arg)));
matches!(
instructions[*idx].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(arg))] == "cm"
)
})
.expect("missing cm load before __exit__");
assert!(
matches!(instructions[cm_load].op, Instruction::LoadFast { .. }),
"CPython codegen_ifexp() emits USE_LABEL(end), then codegen_with_inner() starts the with header after that empty block; flowgraph.c::optimize_load_fast() does not push fallthrough successors from empty blocks, so the context manager load stays strong: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn folded_ifexp_assignment_keeps_later_statement_load_fast_strong() {
let code = compile_exec(
r#"
def f(self, x):
optlevel = 1 if __debug__ else 0
ext = '.pyc'
self.use(x, ext)
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let use_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "use"
}
_ => false,
})
.expect("missing use load");
assert!(
matches!(instructions[use_attr - 1].op, Instruction::LoadFast { .. }),
"CPython codegen_ifexp() emits USE_LABEL(end) for the folded assignment; flowgraph.c::optimize_load_fast() does not push fallthrough from the empty end block, so the next statement receiver stays strong: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn const_assignment_before_with_keeps_context_load_fast_borrowed() {
let code = compile_exec(
r#"
def f(self, cm):
optlevel = 1
with cm as t:
self.use(t, optlevel)
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let first_exit = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadSpecial { method } => {
method.get(OpArg::new(u32::from(u8::from(unit.arg)))) == SpecialMethod::Exit
}
_ => false,
})
.expect("missing __exit__ load");
let cm_load = (0..first_exit)
.rev()
.find(|idx| {
let arg = OpArg::new(u32::from(u8::from(instructions[*idx].arg)));
matches!(
instructions[*idx].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(arg))] == "cm"
)
})
.expect("missing cm load before __exit__");
assert!(
matches!(instructions[cm_load].op, Instruction::LoadFastBorrow { .. }),
"without CPython's folded if-expression end label, flowgraph.c::optimize_load_fast() sees the following with header in the same reachable state and borrows the context manager load: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn if_end_label_reuse_allows_following_return_borrow() {
let code = compile_exec(
r#"
def f(data):
msgids = []
reading_msgid = False
cur_msgid = []
for line in data.split('\n'):
if reading_msgid:
if line.startswith('"'):
cur_msgid.append(line.strip('"'))
else:
msgids.append('\n'.join(cur_msgid))
cur_msgid = []
reading_msgid = False
continue
if line.startswith('msgid '):
line = line[len('msgid '):]
cur_msgid.append(line.strip('"'))
reading_msgid = True
else:
if reading_msgid:
msgids.append('\n'.join(cur_msgid))
return msgids
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let return_load = instructions
.windows(2)
.find(|window| {
let arg = OpArg::new(u32::from(u8::from(window[0].arg)));
matches!(
window[0].op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(arg))] == "msgids"
) && matches!(window[1].op, Instruction::ReturnValue)
})
.unwrap_or_else(|| {
panic!(
"missing return msgids sequence: {:?}",
instructions.iter().map(|unit| unit.op).collect::<Vec<_>>()
)
});
assert!(
matches!(return_load[0].op, Instruction::LoadFastBorrow { .. }),
"CPython codegen_if() ends with USE_LABEL(end), and flowgraph.c::cfg_builder_current_block_is_terminated() reuses the current empty block for that label instead of leaving a b_next barrier before the following return: {:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn named_except_continue_resume_try_body_keeps_method_borrows() {
let code = compile_exec(
r#"
def f(self, block=True):
if not block and not self.wait(timeout=0):
return None
while self.event_queue.empty():
while True:
try:
self.push_char(self.read(1))
except OSError as err:
if err.errno == errno.EINTR:
if not self.event_queue.empty():
return self.event_queue.get()
else:
continue
else:
raise
else:
break
return self.event_queue.get()
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let attr_idx = |name: &str| {
instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == name
}
_ => false,
})
.unwrap_or_else(|| panic!("missing {name} LOAD_ATTR"))
};
let push_char_idx = attr_idx("push_char");
let read_idx = attr_idx("read");
assert!(
matches!(
instructions[push_char_idx - 1].op,
Instruction::LoadFastBorrow { .. }
),
"named-except cleanup continue backedge should not deopt the protected try-body method receiver, got instructions={:?}",
instructions.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
assert!(
matches!(
instructions[read_idx - 1].op,
Instruction::LoadFastBorrow { .. }
),
"nested protected try-body method receiver should remain borrowed like CPython, got instructions={:?}",
instructions.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
}
#[test]
fn boolop_or_shared_body_keeps_false_jump_before_loop_backedge() {
let code = compile_exec(
r#"
def f(value):
digits = []
for digit in value:
if isinstance(digit, int) and 0 <= digit <= 9:
if digits or digit != 0:
digits.append(digit)
else:
raise ValueError
return digits
"#,
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(11).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadSmallInt { .. },
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"OR-shared body should keep CPython last-condition false jump before the loop backedge, got ops={ops:?}"
);
assert!(
!ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"OR-shared body should not be moved after the implicit loop backedge, got ops={ops:?}"
);
}
#[test]
fn single_if_loop_backedge_keeps_true_body_fallthrough_backedge_shape() {
let code = compile_exec(
r##"
def f(buffer, pos, last_char):
while pos > 0:
pos -= 1
if buffer[pos] == "#":
last_char = None
return last_char
"##,
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadConst { .. },
Instruction::StoreFast { .. },
]
)
}),
"single-if loop tail should keep CPython true-body plus fallthrough-backedge shape, got ops={ops:?}",
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadConst { .. },
]
)
}),
"single-if loop tail should not be inverted away from CPython shape, got ops={ops:?}",
);
}
fn location_range(
locations: &(SourceLocation, SourceLocation),
) -> (usize, usize, usize, usize) {
let (location, end_location) = locations;
(
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
)
}
fn instruction_range(
code: &CodeObject,
matches: impl Fn(&Instruction) -> bool,
) -> Option<(usize, usize, usize, usize)> {
code.instructions
.iter()
.zip(&code.locations)
.find_map(|(unit, locations)| matches(&unit.op).then(|| location_range(locations)))
}
fn find_code<'a>(code: &'a CodeObject, name: &str) -> Option<&'a CodeObject> {
if code.obj_name == name {
return Some(code);
}
code.constants.iter().find_map(|constant| {
if let ConstantData::Code { code } = constant {
find_code(code, name)
} else {
None
}
})
}
fn find_direct_child_code<'a>(code: &'a CodeObject, name: &str) -> Option<&'a CodeObject> {
code.constants.iter().find_map(|constant| {
if let ConstantData::Code { code } = constant {
(code.obj_name == name).then_some(code.as_ref())
} else {
None
}
})
}
#[test]
fn annotated_multiline_function_body_keeps_def_firstlineno_like_cpython() {
let code = compile_exec(
r#"
a = 1
def f(
x: a,
): ...
"#,
);
let f = find_code(&code, "f").expect("missing f code");
// CPython 3.14 codegen_function() computes firstlineno from the
// FunctionDef before compiling annotations, then passes it to
// codegen_function_body().
assert_eq!(f.linetable.as_ref(), &[0x80, 0x00, 0xe1, 0x03, 0x06]);
}
#[test]
fn annotation_scope_return_uses_function_location_like_cpython() {
let code = compile_exec(
r#"
def g():
def f(x: not (int is int), /): ...
"#,
);
let g = find_code(&code, "g").expect("missing g code");
let annotate = find_code(g, "__annotate__").expect("missing annotation code");
// CPython 3.14 codegen_function_annotations() receives LOC(function)
// and uses it for the annotation closure's BUILD_MAP/RETURN_VALUE and
// for the parent MAKE_FUNCTION annotate sequence.
assert_eq!(g.linetable.as_ref(), &[0x80, 0x00, 0xdf, 0x04, 0x26]);
assert_eq!(
annotate.linetable.as_ref(),
&[
0x80, 0x00, 0xd7, 0x04, 0x26, 0xd1, 0x04, 0x26, 0x94, 0x23, 0x9c, 0x13, 0xd0, 0x0d,
0x1d, 0xd1, 0x04, 0x26,
],
);
}
#[test]
fn starred_arg_annotation_unpack_uses_function_location_like_cpython() {
let code = compile_exec("def f(*args: *Ts): pass\n");
let annotate = find_code(&code, "__annotate__").expect("missing annotation code");
// codegen_argannotation() visits `Ts` at the annotation
// expression location, then emits UNPACK_SEQUENCE at LOC(function).
assert_eq!(
annotate.linetable.as_ref(),
&[
0x80, 0x00, 0xd7, 0x00, 0x17, 0xd1, 0x00, 0x17, 0x8c, 0x62, 0xd3, 0x00, 0x17
],
);
}
#[test]
fn module_deferred_annotations_use_start_location_like_cpython() {
let code = compile_exec(
"\
import os
X: int
Y: str
",
);
let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code");
// compile.c::start_location() passes the first module
// statement location into _PyCodegen_Module(), and
// codegen_process_deferred_annotations() uses that loc for annotation
// scope setup, BUILD_MAP, STORE_SUBSCR, and RETURN_VALUE.
assert_eq!(
annotate.linetable.as_ref(),
&[
0x80, 0x00, 0x87, 0x09, 0x81, 0x09, 0xdf, 0x00, 0x06, 0x82, 0x06, 0x84, 0x33, 0x81,
0x06, 0xf1, 0x03, 0x00, 0x01, 0x0a, 0xe7, 0x00, 0x06, 0x82, 0x06, 0x84, 0x33, 0x81,
0x06, 0xf2, 0x05, 0x00, 0x01, 0x0a,
]
);
}
#[test]
fn super_method_call_kw_names_use_attribute_location_like_cpython() {
let code = compile_exec(
"\
class C:
def f(self, x, y):
super().__init__(
x=x,
y=y)
",
);
let f = find_code(&code, "f").expect("missing f code");
let call_kw_index = f
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::CallKw { .. }))
.expect("missing CALL_KW");
let (kw_names, (location, end_location)) = f
.instructions
.iter()
.zip(&f.locations)
.take(call_kw_index)
.rev()
.find(|(unit, _)| matches!(unit.op, Instruction::LoadConst { .. }))
.expect("missing CALL_KW names tuple");
assert!(
matches!(kw_names.op, Instruction::LoadConst { .. }),
"expected keyword names tuple before CALL_KW"
);
assert_eq!(
(location.line.get(), end_location.line.get()),
(3, 3),
"CPython maybe_optimize_method_call() passes the updated method-attribute loc into codegen_call_simple_kw_helper()"
);
}
#[test]
fn multiline_super_method_load_uses_expression_start_location_like_cpython() {
let code = compile_exec(
"\
class C:
def f(self):
return super(
).m()
",
);
let f = find_code(&code, "f").expect("missing f code");
let load_super_index = f
.instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadSuperAttr { namei } => namei
.get(OpArg::new(u32::from(u8::from(unit.arg))))
.is_load_method(),
_ => false,
})
.expect("missing LOAD_SUPER_METHOD");
let (load_location, _) = f.locations[load_super_index];
assert_eq!(
load_location.line.get(),
3,
"CPython maybe_optimize_method_call() emits LOAD_SUPER_METHOD at LOC(meth), before updating to the attribute start"
);
}
#[test]
fn multiline_non_ascii_attribute_uses_cpython_unicode_length() {
let code = compile_exec(
"\
def f(obj):
return (obj
.é)
",
);
let f = find_code(&code, "f").expect("missing f code");
let load_attr_position = f
.instructions
.iter()
.zip(&f.locations)
.find_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::LoadAttr { .. }).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.expect("missing LOAD_ATTR");
assert_eq!(
load_attr_position,
(3, 11, 3, 12),
"CPython update_start_location_to_match_attr() subtracts PyUnicode_GET_LENGTH(attr), not the UTF-8 byte length; Rust SourceLocation exposes the resulting columns as one-based"
);
}
#[test]
fn two_arg_super_attr_in_class_body_is_optimized_like_cpython() {
let code = compile_exec(
"\
class C:
x = super(C, self).attr
",
);
let class_code = find_code(&code, "C").expect("missing class code");
assert!(
class_code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadSuperAttr { .. })),
"CPython can_optimize_super_call() does not require function scope for two-argument super()"
);
}
#[test]
fn module_super_symbol_blocks_zero_arg_super_optimization_like_cpython() {
let code = compile_exec(
"\
super
class C:
def f(self):
return super().attr
",
);
let f = find_code(&code, "f").expect("missing f code");
assert!(
!f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadSuperAttr { .. })),
"CPython can_optimize_super_call() rejects any top-level symbol-table entry for super"
);
}
#[test]
fn lambda_return_uses_body_location_like_cpython() {
let code = compile_exec(
"\
def outer():
return lambda x: x if x else 1
",
);
let lambda = find_code(&code, "<lambda>").expect("missing lambda code");
let return_positions: Vec<_> = lambda
.instructions
.iter()
.zip(&lambda.locations)
.filter_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::ReturnValue).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.collect();
assert_eq!(
return_positions,
vec![(2, 22, 2, 35), (2, 22, 2, 35)],
"CPython codegen_lambda() emits RETURN_VALUE at LOC(lambda body)"
);
}
#[test]
fn explicit_return_value_locations_match_cpython_codegen_return() {
let code = compile_exec(
"\
def dynamic(x):
return x
def constant():
return 1
def bare():
return
",
);
let cases = [
("dynamic", vec![(2, 5, 2, 13)]),
("constant", vec![(5, 12, 5, 13)]),
("bare", vec![(8, 5, 8, 11)]),
];
for (name, expected) in cases {
let function = find_code(&code, name).expect("missing function code");
let return_positions: Vec<_> = function
.instructions
.iter()
.zip(&function.locations)
.filter_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::ReturnValue).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.collect();
assert_eq!(
return_positions, expected,
"CPython codegen_return() emits explicit return at loc for {name}"
);
}
}
#[test]
fn continue_jump_keeps_statement_location_like_cpython() {
let code = compile_exec(
"\
def continues(xs):
for x in xs:
if x:
continue
use(x)
",
);
{
let (name, expected_position) = ("continues", (4, 13, 4, 21));
let function = find_code(&code, name).expect("missing function code");
let jump_positions: Vec<_> = function
.instructions
.iter()
.zip(&function.locations)
.filter_map(|(unit, (location, end_location))| {
matches!(
unit.op,
Instruction::JumpForward { .. } | Instruction::JumpBackward { .. }
)
.then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.collect();
assert!(
jump_positions.contains(&expected_position),
"CPython codegen_continue() emits final jump at statement loc for {name}, got {jump_positions:?}"
);
}
}
#[test]
fn not_compare_uses_unary_location_like_cpython() {
let code = compile_exec(
"\
def f(self, other):
return not self == other
",
);
let f = find_code(&code, "f").expect("missing f code");
// CPython 3.14 parses the Compare inside UnaryOp(Not) with the
// UnaryOp start location, so codegen_compare() emits COMPARE_OP at
// the full "not self == other" range before flowgraph folds TO_BOOL.
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x0f, 0x13, 0xd2, 0x0b, 0x1c, 0xd0, 0x04, 0x1c,
]
);
}
#[test]
fn not_chained_compare_keeps_compare_location_like_cpython() {
let code = compile_exec(
"\
def f(c):
return not (b\" \" <= c <= b\"~\")
",
);
let f = find_code(&code, "f").expect("missing f code");
// CPython's single Compare under UnaryOp(Not) includes "not" in the
// Compare range, but chained comparisons keep their inner range for
// compare scaffolding and only use the UnaryOp range for TO_BOOL and
// UNARY_NOT.
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x10, 0x14, 0x98, 0x01, 0xd7, 0x10, 0x21, 0xd4, 0x10, 0x21, 0x98,
0x54, 0xd1, 0x10, 0x21, 0xd4, 0x0b, 0x22, 0xd0, 0x04, 0x22, 0xd1, 0x10, 0x21, 0xd4,
0x0b, 0x22, 0xd0, 0x04, 0x22,
]
);
}
#[test]
fn type_param_scopes_use_cpython_locations() {
let code = compile_exec("type BoundGenericAlias[X: int] = set[X]\n");
let type_params = find_code(&code, "<generic parameters of BoundGenericAlias>")
.expect("missing generic parameters code");
let bound = find_direct_child_code(type_params, "X").expect("missing X bound code");
let alias =
find_direct_child_code(type_params, "BoundGenericAlias").expect("missing alias code");
// CPython 3.14 codegen_type_params() emits type-parameter ops at
// LOC(typeparam), bound/default evaluator ops at LOC(e), and type alias
// body plumbing at LOC(s).
assert_eq!(
type_params.linetable.as_ref(),
&[
0xf8, 0x80, 0x00, 0xd0, 0x00, 0x27, 0x90, 0x76, 0x9b, 0x23, 0x93, 0x76, 0xd7, 0x00,
0x27, 0xd1, 0x00, 0x27,
],
);
assert_eq!(
bound.linetable.as_ref(),
&[0x80, 0x00, 0x9f, 0x23, 0x9e, 0x23]
);
assert_eq!(
alias.linetable.as_ref(),
&[
0xf8, 0x80, 0x00, 0xd7, 0x00, 0x27, 0xd0, 0x00, 0x27, 0xa4, 0x13, 0xa0, 0x51, 0xa5,
0x16, 0xd0, 0x00, 0x27,
],
);
}
#[test]
fn typealias_value_scope_has_single_return_like_cpython() {
let code = compile_exec("type Alias = int\n");
let alias = find_direct_child_code(&code, "Alias").expect("missing alias code");
let return_count = alias
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::ReturnValue))
.count();
assert_eq!(
return_count, 1,
"CPython codegen_typealias_body() emits one RETURN_VALUE and assembles with addNone=0, got instructions={:?}",
alias.instructions
);
}
#[test]
fn generic_typealias_wrapper_return_uses_alias_location_like_cpython() {
let code = compile_exec("type A[T] = T\n");
let type_params =
find_code(&code, "<generic parameters of A>").expect("missing type params code");
// codegen_typealias() assembles the generic-parameters
// wrapper with addNone=0 after codegen_typealias_body() leaves the type
// alias object on the stack. The final RETURN_VALUE keeps LOC(type alias).
assert_eq!(
type_params.linetable.as_ref(),
&[
0xf8, 0x80, 0x00, 0x80, 0x0d, 0x84, 0x71, 0x87, 0x0d, 0x81, 0x0d
],
);
}
#[test]
fn type_param_bound_scope_has_single_return_like_cpython() {
let code = compile_exec("type Alias[T: int] = T\n");
let type_params =
find_code(&code, "<generic parameters of Alias>").expect("missing type params code");
let bound = find_direct_child_code(type_params, "T").expect("missing T bound code");
let return_count = bound
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::ReturnValue))
.count();
assert_eq!(
return_count, 1,
"CPython codegen_type_param_bound_or_default() emits one explicit RETURN_VALUE before OptimizeAndAssemble(addNone=1), got instructions={:?}",
bound.instructions
);
}
#[test]
fn class_body_scope_has_single_return_like_cpython() {
let code = compile_exec("class C:\n pass\n");
let class_code = find_code(&code, "C").expect("missing class code");
let return_count = class_code
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::ReturnValue))
.count();
assert_eq!(
return_count, 1,
"CPython codegen_class_body() emits one explicit RETURN_VALUE before OptimizeAndAssemble(addNone=1), got instructions={:?}",
class_code.instructions
);
}
#[test]
fn generic_function_annotation_scope_uses_function_location_like_cpython() {
let code = compile_exec("def f[T](x: int): ...\n");
let type_params =
find_code(&code, "<generic parameters of f>").expect("missing type params code");
let annotate =
find_direct_child_code(type_params, "__annotate__").expect("missing annotation code");
// CPython 3.14 passes LOC(function) into codegen_function_annotations(),
// even when the annotation closure is emitted inside the generic
// parameters scope after codegen_type_params().
assert_eq!(
annotate.linetable.as_ref(),
&[
0x80, 0x00, 0xd7, 0x00, 0x15, 0xd1, 0x00, 0x15, 0x8c, 0x43, 0xd1, 0x00, 0x15,
],
);
}
#[test]
fn decorated_generic_function_type_params_use_decorator_firstlineno_like_cpython() {
let code = compile_exec(
"\
def deco(obj): return obj
@deco
def f[T](): pass
",
);
let type_params =
find_code(&code, "<generic parameters of f>").expect("missing type params code");
// codegen_function() passes firstlineno, not LOC(s).lineno, to
// the generic-parameters scope.
assert_eq!(type_params.first_line_number.unwrap().get(), 2);
}
#[test]
fn generic_class_type_params_store_uses_class_location_like_cpython() {
let code = compile_exec(
"\
def outer():
class X[T]: ...
",
);
let type_params =
find_code(&code, "<generic parameters of X>").expect("missing type params code");
// CPython 3.14 codegen_class() calls codegen_type_params(), then stores
// the resulting .type_params cell with codegen_nameop(c, LOC(class), ...).
assert_eq!(
type_params.linetable.as_ref(),
&[
0xf8, 0x80, 0x00, 0x8c, 0x41, 0x87, 0x4f, 0x87, 0x4f, 0x80, 0x4f,
]
);
}
#[test]
fn generic_class_wrapper_ops_use_class_location_like_cpython() {
let code = compile_exec(
"\
def f():
class X[T](tuple):
pass
",
);
let f = find_code(&code, "f").expect("missing function code");
let wrapper_positions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Resume { .. }))
.take(4)
.zip(f.locations.iter().filter(|_| true).skip(1))
.map(|(unit, (location, end_location))| {
(
unit.op,
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
)
})
.collect();
assert_eq!(
wrapper_positions
.iter()
.map(|(_, line, col, end_line, end_col)| (*line, *col, *end_line, *end_col))
.collect::<Vec<_>>(),
vec![(2, 5, 3, 13); 4],
"CPython codegen_class() emits type-params wrapper closure, PUSH_NULL, and CALL at LOC(class)"
);
let type_params =
find_code(f, "<generic parameters of X>").expect("missing generic parameters code");
let generic_base_position = type_params
.instructions
.iter()
.zip(&type_params.locations)
.find_map(|(unit, (location, end_location))| {
let Instruction::LoadFastBorrow { var_num } = unit.op else {
return None;
};
let idx = var_num.get(OpArg::new(u32::from(u8::from(unit.arg))));
let localsplus = type_params
.varnames
.iter()
.chain(type_params.cellvars.iter())
.chain(type_params.freevars.iter())
.collect::<Vec<_>>();
localsplus
.get(usize::from(idx))
.is_some_and(|name| name.as_str() == ".generic_base")
.then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.expect("missing .generic_base load");
assert_eq!(
generic_base_position,
(2, 5, 3, 13),
"CPython codegen_class() injects .generic_base with LOC(class)"
);
}
#[test]
fn try_except_else_finally_child_scopes_follow_cpython_symbol_order() {
let code = compile_exec(
"\
def f(x):
try:
pass
except Exception:
y = 1
def h():
return y
else:
def e():
return x
finally:
def z():
return x
",
);
let f = find_code(&code, "f").expect("missing function code");
let h = find_code(f, "h").expect("missing handler function code");
let e = find_code(f, "e").expect("missing else function code");
let z = find_code(f, "z").expect("missing finally function code");
assert_eq!(
h.freevars
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>(),
vec!["y"],
"handler child scope should consume the handler symbol table"
);
assert_eq!(
e.freevars
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>(),
vec!["x"],
"else child scope should be consumed before handler scopes, matching CPython codegen_try_except()"
);
assert_eq!(
z.freevars
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>(),
vec!["x"],
"finally child scope should remain after body/else/handler scopes"
);
}
#[test]
fn try_star_child_scopes_follow_codegen_order_like_cpython() {
let code = compile_exec(
"\
def f(x):
try:
pass
except* Exception:
y = 1
def h():
return y
else:
def e():
return x
",
);
let f = find_code(&code, "f").expect("missing function code");
let h = find_code(f, "h").expect("missing except* handler function code");
let e = find_code(f, "e").expect("missing else function code");
assert_eq!(
h.freevars
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>(),
vec!["y"],
"except* handler child scope should consume handler symbol table before else"
);
assert_eq!(
e.freevars
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>(),
vec!["x"],
"except* else child scope should be consumed after handler scopes, matching CPython codegen_try_star_except()"
);
}
#[test]
fn function_default_and_decorator_child_scopes_follow_cpython_symbol_order() {
fn direct_child_codes<'a>(code: &'a CodeObject, name: &str) -> Vec<&'a CodeObject> {
code.constants
.iter()
.filter_map(|constant| {
if let ConstantData::Code { code } = constant
&& code.obj_name == name
{
Some(code.as_ref())
} else {
None
}
})
.collect()
}
let code = compile_exec(
"\
def outer(x, deco):
@(lambda f: deco(f))
def inner(a=(lambda: x)()):
return a
",
);
let outer = find_code(&code, "outer").expect("missing outer function code");
let lambdas = direct_child_codes(outer, "<lambda>");
assert_eq!(lambdas.len(), 2);
assert_eq!(
lambdas[0]
.freevars
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>(),
vec!["deco"],
"decorator lambda is emitted first by codegen_function()"
);
assert_eq!(
lambdas[1]
.freevars
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>(),
vec!["x"],
"default lambda should still consume the default symbol table"
);
}
#[test]
fn decorated_generic_class_type_params_use_decorator_firstlineno_like_cpython() {
let code = compile_exec(
"\
def deco(obj): return obj
@deco
class C[T]: pass
",
);
let type_params =
find_code(&code, "<generic parameters of C>").expect("missing type params code");
// codegen_class() also enters the generic-parameters scope with
// firstlineno, which is the first decorator line when decorators exist.
assert_eq!(type_params.first_line_number.unwrap().get(), 2);
}
#[test]
fn class_deferred_annotations_use_class_body_location_like_cpython() {
let code = compile_exec(
r#"
class C:
"doc"
x: int
"#,
);
let class_code = find_code(&code, "C").expect("missing class code");
// CPython 3.14 calls codegen_body(c, loc, ...) from codegen_class_body()
// with LOCATION(firstlineno, firstlineno, 0, 0). Deferred annotation
// closure setup and following artificial class tail inherit that class
// body location, not the annotation expression location.
assert_eq!(
class_code.linetable.as_ref(),
&[
0xf8, 0x87, 0x00, 0x80, 0x00, 0xd9, 0x04, 0x09, 0xf7, 0x03, 0x00, 0x01, 0x01, 0x83,
0x00,
],
);
}
#[test]
fn future_annotation_string_uses_annotation_location_like_cpython() {
let code = compile_exec("from __future__ import annotations\nclass Bar:\n foo: Foo\n");
let class_code = find_code(&code, "Bar").expect("missing class code");
// CPython 3.14 codegen_annassign() calls codegen_visit_annexpr(),
// which emits the stringized annotation at LOC(annotation), then emits
// the __annotations__ store sequence at LOC(AnnAssign).
assert_eq!(
class_code.linetable.as_ref(),
&[0x87, 0x00, 0xd8, 0x09, 0x0c, 0x87, 0x48]
);
}
#[test]
#[expect(
clippy::literal_string_with_formatting_args,
reason = "the literal is the expected t-string annotation"
)]
fn future_tstring_annotation_preserves_interpolation_source_like_cpython() {
let code = compile_exec(
"from __future__ import annotations\nx: t'{a + b}'\ny: t'{ a + b }'\nz: f'{a + b =}'\nu: t'{a + b =}'\nv: t'{a + b =:>10}'\np: t'{(a)}'\nq: t'{((a))!r}'\nr: t'{ ((a)) = !r:>10}'\ns: t'{(a)=}'\nt: t'{a == b = }'\na1: t'''{a= # x=y\n}'''\na2: t'''{a # x=y\n}'''\na3: t'''{(a # x=y\n)}'''\na4: t'''{a # x=y\n!r}'''\na5: t'''{a # x=y\n:>10}'''\na6: t'''{'#'}'''\na7: t'''{('#', a) # c=d\n}'''\n",
);
let annotation_strings = code
.constants
.iter()
.filter_map(|constant| match constant {
ConstantData::Str { value }
if value.starts_with("t'")
|| value.starts_with("t\"")
|| value.starts_with("f'") =>
{
Some(value.to_string())
}
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(
annotation_strings,
[
"t'{a + b}'",
"t'{ a + b}'",
"f'a + b ={a + b!r}'",
"t'a + b ={a + b!r}'",
"t'a + b ={a + b:>10}'",
"t'{(a)}'",
"t'{((a))!r}'",
"t' ((a)) = { ((a))!r:>10}'",
"t'(a)={(a)!r}'",
"t'a == b = {a == b!r}'",
"t'a= \\n{a!r}'",
"t'{a}'",
"t'{(a \\n)}'",
"t'{a!r}'",
"t'{a:>10}'",
"t\"{'#'}\"",
"t\"{('#', a)}\"",
]
);
}
#[test]
fn lambda_dict_literal_ops_use_dict_location_like_cpython() {
let code = compile_exec(
"\
f = lambda data: {'x': data}
g = lambda i: {**i}
",
);
let f = find_code(&code, "<lambda>").expect("missing f lambda code");
let g = code
.constants
.iter()
.filter_map(|constant| {
if let ConstantData::Code { code } = constant {
(code.obj_name == "<lambda>").then_some(code.as_ref())
} else {
None
}
})
.nth(1)
.expect("missing g lambda code");
// CPython 3.14 codegen_dict()/codegen_subdict() uses LOC(dict) for
// BUILD_MAP, MAP_ADD, and DICT_UPDATE, so the lambda RETURN_VALUE
// inherits the full dict literal location after compiling its body.
assert_eq!(
f.linetable.as_ref(),
&[0x80, 0x00, 0x90, 0x23, 0x90, 0x74, 0x91, 0x1b]
);
assert_eq!(
g.linetable.as_ref(),
&[0x80, 0x00, 0x88, 0x65, 0x90, 0x11, 0x89, 0x65]
);
}
#[test]
fn dict_unpacking_large_regular_run_uses_subdict_chunks_like_cpython() {
let pairs = (0..17)
.map(|i| format!("{i}: {i}"))
.collect::<Vec<_>>()
.join(", ");
let source = format!("def f(x):\n return {{{pairs}, **x}}\n");
let code = compile_exec(&source);
let f = find_code(&code, "f").expect("missing f code");
let first_dict_update = f
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::DictUpdate { .. }))
.expect("missing DICT_UPDATE");
let prefix = &f.instructions[..first_dict_update];
let build_map_args: Vec<_> = prefix
.iter()
.filter_map(|unit| {
matches!(unit.op, Instruction::BuildMap { .. }).then_some(u8::from(unit.arg))
})
.collect();
let map_adds = prefix
.iter()
.filter(|unit| matches!(unit.op, Instruction::MapAdd { .. }))
.count();
assert_eq!(
build_map_args,
vec![0],
"CPython codegen_dict() routes a 17-pair run before ** through codegen_subdict(), got instructions={:?}",
f.instructions
);
assert_eq!(
map_adds, 17,
"CPython codegen_subdict() uses MAP_ADD for all 17 pairs before **, got instructions={:?}",
f.instructions
);
}
#[test]
fn class_function_like_scopes_set_method_flag_like_cpython() {
let code = compile_exec_with_options(
r#"
class C:
def m(self):
pass
async def am(self):
pass
f = lambda self: self
y = (i for i in ())
def f():
pass
"#,
CompileOpts::default(),
);
let class_code = find_code(&code, "C").expect("missing class code");
let method = find_code(class_code, "m").expect("missing method code");
let async_method = find_code(class_code, "am").expect("missing async method code");
let lambda = find_code(class_code, "<lambda>").expect("missing lambda code");
let genexpr = find_code(class_code, "<genexpr>").expect("missing genexpr code");
let module_function = find_code(&code, "f").expect("missing module function code");
for code in [method, async_method, lambda, genexpr] {
assert!(
code.flags.contains(bytecode::CodeFlags::METHOD),
"class-scope function-like code should carry CO_METHOD like CPython 3.14, got {:?}",
code.flags
);
}
assert!(
!module_function.flags.contains(bytecode::CodeFlags::METHOD),
"module-scope function must not carry CO_METHOD"
);
}
#[test]
fn inlined_comprehension_lambda_in_class_is_not_method_like_cpython() {
let code = compile_exec(
"\
class C:
def method(self):
super()
return __class__
items = [(lambda: i) for i in range(5)]
",
);
let class_code = find_code(&code, "C").expect("missing class code");
let lambda = find_code(class_code, "<lambda>").expect("missing lambda code");
assert!(
lambda.flags.contains(bytecode::CodeFlags::NESTED),
"lambda under inlined class comprehension should stay nested"
);
assert!(
!lambda.flags.contains(bytecode::CodeFlags::METHOD),
"CPython creates this lambda while the current symtable block is the comprehension, not the class"
);
}
#[test]
fn class_inlined_comprehension_pushes_only_bound_locals_like_cpython() {
let code = compile_exec(
"\
class C:
x = 1
items = [x for i in range(3)]
",
);
let class_code = find_code(&code, "C").expect("missing class code");
let cleared_names = class_code
.instructions
.iter()
.filter_map(|unit| match unit.op {
Instruction::LoadFastAndClear { var_num } => {
let idx = var_num.get(OpArg::new(u32::from(u8::from(unit.arg))));
Some(class_code.varnames[usize::from(idx)].as_str())
}
_ => None,
})
.collect::<Vec<_>>();
assert!(
cleared_names.contains(&"i"),
"the comprehension iteration variable should be isolated, got {cleared_names:?}"
);
assert!(
!cleared_names.contains(&"x"),
"CPython applies the class-block special case while tweaking scopes, but codegen_push_inlined_comprehension_locals() runs after u_in_inlined_comp is set and only clears DEF_LOCAL names; got {cleared_names:?}"
);
}
#[test]
fn genexpr_implicit_iterator_is_not_posonly_like_cpython() {
let code = compile_exec("x = (i for i in ())");
let genexpr = find_code(&code, "<genexpr>").expect("missing genexpr code");
assert_eq!(genexpr.arg_count, 1);
assert_eq!(
genexpr.posonlyarg_count, 0,
"CPython codegen_comprehension() sets u_argcount=1 and leaves u_posonlyargcount=0"
);
}
#[test]
fn posonly_function_argcount_metadata_matches_cpython_assemble_split() {
let code = compile_exec("def f(a, /, b):\n pass\n");
let func = find_code(&code, "f").expect("missing function code");
assert_eq!(
func.arg_count, 2,
"CPython assemble.c exposes co_argcount as u_posonlyargcount + u_argcount"
);
assert_eq!(func.posonlyarg_count, 1);
assert_eq!(func.varnames.as_ref(), &["a".to_owned(), "b".to_owned()]);
assert_eq!(
func.localspluskinds[0] & (CO_FAST_ARG_POS | CO_FAST_ARG_KW),
CO_FAST_ARG_POS,
"CPython compute_localsplus_info marks only u_posonlyargcount slots as positional-only"
);
assert_eq!(
func.localspluskinds[1] & (CO_FAST_ARG_POS | CO_FAST_ARG_KW),
CO_FAST_ARG_POS | CO_FAST_ARG_KW,
"CPython compute_localsplus_info marks u_argcount slots after posonly as positional-or-keyword"
);
}
#[test]
fn async_generator_uses_cpython_async_generator_flag() {
let code = compile_exec_with_options(
r#"
def g():
yield 1
async def c():
return 1
async def ag():
yield 1
"#,
CompileOpts::default(),
);
let generator = find_code(&code, "g").expect("missing generator code");
let coroutine = find_code(&code, "c").expect("missing coroutine code");
let async_generator = find_code(&code, "ag").expect("missing async generator code");
assert!(generator.flags.contains(bytecode::CodeFlags::GENERATOR));
assert!(!generator.flags.contains(bytecode::CodeFlags::COROUTINE));
assert!(
!generator
.flags
.contains(bytecode::CodeFlags::ASYNC_GENERATOR)
);
assert!(coroutine.flags.contains(bytecode::CodeFlags::COROUTINE));
assert!(!coroutine.flags.contains(bytecode::CodeFlags::GENERATOR));
assert!(
!coroutine
.flags
.contains(bytecode::CodeFlags::ASYNC_GENERATOR)
);
assert!(
async_generator
.flags
.contains(bytecode::CodeFlags::ASYNC_GENERATOR)
);
assert!(
!async_generator
.flags
.contains(bytecode::CodeFlags::GENERATOR)
);
assert!(
!async_generator
.flags
.contains(bytecode::CodeFlags::COROUTINE)
);
}
#[test]
fn is_none_jump_preserves_cpython_const_order() {
let code = compile_exec_with_options(
r#"
def f(self, payload):
"doc"
if self.x is None:
self.x = [payload]
else:
raise TypeError("bad")
"#,
CompileOpts::default(),
);
let function = find_code(&code, "f").expect("missing function code");
assert!(
matches!(
function.constants.as_ref(),
[
ConstantData::Str { value: doc },
ConstantData::None,
ConstantData::Str { value: message },
] if doc.as_ref() == "doc" && message.as_ref() == "bad"
),
"CPython registers None from the pre-folded `is None` comparison before the else-body string"
);
}
#[test]
fn stop_iteration_handler_starts_at_scope_start_resume_like_cpython() {
let code = compile_exec_with_options(
r#"
def g():
yield 1
async def c():
return 1
x = (i for i in ())
"#,
CompileOpts::default(),
);
fn assert_stop_iteration_table_starts_at_resume(code: &CodeObject) {
let resume_idx = u32::try_from(
code.instructions
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::Resume { context }
if matches!(
context
.get(OpArg::new(u32::from(u8::from(unit.arg))))
.location(),
oparg::ResumeLocation::AtFuncStart
)
)
})
.expect("missing function-start RESUME"),
)
.unwrap();
let entries = bytecode::decode_exception_table(&code.exceptiontable);
assert!(
entries.iter().any(|entry| entry.start == resume_idx),
"CPython codegen_wrap_in_stopiteration_handler() inserts SETUP_CLEANUP before RESUME so the StopIteration table starts at RESUME; resume_idx={resume_idx}, entries={entries:?}, instructions={:?}",
code.instructions
);
}
assert_stop_iteration_table_starts_at_resume(find_code(&code, "g").expect("missing g"));
assert_stop_iteration_table_starts_at_resume(find_code(&code, "c").expect("missing c"));
assert_stop_iteration_table_starts_at_resume(
find_code(&code, "<genexpr>").expect("missing genexpr"),
);
}
#[test]
fn inlined_comprehension_cleanup_starts_at_result_build_like_cpython() {
let code = compile_exec_with_options(
r#"
def f(self):
return [k for k, v in self._headers]
"#,
CompileOpts::default(),
);
let f = find_code(&code, "f").expect("missing f");
let build_list_idx = u32::try_from(
f.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::BuildList { .. }))
.expect("missing BUILD_LIST"),
)
.unwrap();
let entries = bytecode::decode_exception_table(&f.exceptiontable);
assert!(
entries.iter().any(|entry| {
entry.start == build_list_idx && entry.depth == 3 && !entry.push_lasti
}),
"CPython codegen_push_inlined_comprehension_locals() emits SETUP_FINALLY before BUILD_LIST, so the virtual cleanup table starts at BUILD_LIST with saved locals depth; build_list_idx={build_list_idx}, entries={entries:?}, instructions={:?}",
f.instructions
);
}
#[test]
fn or_return_not_taken_before_jump_target_splits_exception_table_like_cpython() {
let code = compile_exec_with_options(
r#"
def f(self, maintype):
if maintype != "multipart" or not self.is_multipart():
return
yield 1
"#,
CompileOpts::default(),
);
let f = find_code(&code, "f").expect("missing f");
let not_taken_before_return = u32::try_from(
f.instructions
.windows(3)
.position(|window| {
matches!(
window,
[
CodeUnit {
op: Instruction::NotTaken,
..
},
CodeUnit {
op: Instruction::LoadConst { .. },
..
},
CodeUnit {
op: Instruction::ReturnValue,
..
},
]
)
})
.expect("missing NOT_TAKEN before return"),
)
.unwrap();
let return_load = not_taken_before_return + 1;
let entries = bytecode::decode_exception_table(&f.exceptiontable);
assert!(
entries.iter().all(|entry| {
not_taken_before_return < entry.start || not_taken_before_return >= entry.end
}),
"CPython normalize_jumps() can leave a NOT_TAKEN before a separately labelled jump target outside the generator StopIteration range; entries={entries:?}, instructions={:?}",
f.instructions
);
assert!(
entries
.iter()
.any(|entry| entry.start <= return_load && return_load < entry.end),
"the return block after that NOT_TAKEN is still protected by the StopIteration handler; entries={entries:?}, instructions={:?}",
f.instructions
);
}
#[test]
fn loop_break_condition_splits_exception_table_like_cpython() {
let code = compile_exec_with_options(
r#"
def f(start, items):
if start:
for x in items:
if x == start:
break
yield 1
"#,
CompileOpts::default(),
);
let f = find_code(&code, "f").expect("missing f");
let break_jump = u32::try_from(
f.instructions
.windows(3)
.position(|window| {
matches!(
window,
[
CodeUnit {
op: Instruction::PopJumpIfTrue { .. },
..
},
CodeUnit {
op: Instruction::Cache,
..
},
CodeUnit {
op: Instruction::NotTaken,
..
},
]
) || matches!(
window,
[
CodeUnit {
op: Instruction::PopJumpIfTrue { .. },
..
},
CodeUnit {
op: Instruction::NotTaken,
..
},
CodeUnit {
op: Instruction::JumpBackward { .. },
..
},
]
)
})
.expect("missing loop break conditional jump"),
)
.unwrap();
let entries = bytecode::decode_exception_table(&f.exceptiontable);
assert!(
entries
.iter()
.all(|entry| break_jump < entry.start || break_jump >= entry.end),
"CPython normalize_jumps() leaves the loop-break conditional before the synthetic NOT_TAKEN/JUMP_BACKWARD block outside the StopIteration table; break_jump={break_jump}, entries={entries:?}, instructions={:?}",
f.instructions
);
}
#[test]
fn nested_ifexp_not_taken_splits_exception_table_like_cpython() {
let code = compile_exec_with_options(
r#"
def f(flag, subparts):
if flag:
candidate = subparts[0] if subparts else None
yield 1
"#,
CompileOpts::default(),
);
let f = find_code(&code, "f").expect("missing f");
let conditional_expr_not_taken = u32::try_from(
f.instructions
.iter()
.enumerate()
.find_map(|(idx, unit)| {
if !matches!(unit.op, Instruction::NotTaken) {
return None;
}
let prev = f.instructions[..idx]
.iter()
.rev()
.find(|unit| !matches!(unit.op, Instruction::Cache))?;
let mut following = f.instructions[idx + 1..]
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache));
let next = following.next()?;
let after_next = following.next()?;
(matches!(prev.op, Instruction::PopJumpIfFalse { .. })
&& matches!(next.op, Instruction::LoadFastBorrow { .. })
&& matches!(after_next.op, Instruction::LoadSmallInt { .. }))
.then_some(idx)
})
.expect("missing conditional expression NOT_TAKEN"),
)
.unwrap();
let body_start = conditional_expr_not_taken + 1;
let entries = bytecode::decode_exception_table(&f.exceptiontable);
assert!(
entries.iter().all(|entry| {
conditional_expr_not_taken < entry.start || conditional_expr_not_taken >= entry.end
}),
"CPython codegen_ifexp() uses a separate orelse label inside conditional statements, leaving the normalize_jumps NOT_TAKEN outside the StopIteration table; not_taken={conditional_expr_not_taken}, entries={entries:?}, instructions={:?}",
f.instructions
);
assert!(
entries
.iter()
.any(|entry| entry.start <= body_start && body_start < entry.end),
"the conditional-expression body after that NOT_TAKEN remains protected; body_start={body_start}, entries={entries:?}, instructions={:?}",
f.instructions
);
}
#[test]
fn bool_not_taken_after_conditional_yield_splits_like_cpython() {
let code = compile_exec_with_options(
r#"
def f(a, b, c):
if a:
yield 1
if b:
x = 2
if c:
x = 3
yield 4
"#,
CompileOpts::default(),
);
let f = find_code(&code, "f").expect("missing f");
let split_not_taken = f
.instructions
.iter()
.enumerate()
.filter_map(|(idx, unit)| {
if !matches!(unit.op, Instruction::NotTaken) {
return None;
}
let prev = f.instructions[..idx]
.iter()
.rev()
.find(|unit| !matches!(unit.op, Instruction::Cache))?;
matches!(
prev.op,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }
)
.then(|| u32::try_from(idx).unwrap())
})
.nth(1)
.expect("missing second bool conditional NOT_TAKEN");
let entries = bytecode::decode_exception_table(&f.exceptiontable);
assert!(
entries
.iter()
.all(|entry| split_not_taken < entry.start || split_not_taken >= entry.end),
"CPython labels exception targets before normalize_jumps(), so the general bool-jump NOT_TAKEN after a conditional yield is outside the StopIteration table; not_taken={split_not_taken}, entries={entries:?}, instructions={:?}",
f.instructions
);
}
fn non_cache_instructions(code: &CodeObject) -> impl Iterator<Item = &CodeUnit> {
code.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
}
fn full_opargs_for(
code: &CodeObject,
mut predicate: impl FnMut(Instruction) -> bool,
) -> Vec<u32> {
let mut extended = 0u32;
let mut args = Vec::new();
for unit in non_cache_instructions(code) {
let byte = u32::from(u8::from(unit.arg));
if matches!(unit.op, Instruction::ExtendedArg) {
extended = (extended << 8) | byte;
continue;
}
let oparg = (extended << 8) | byte;
extended = 0;
if predicate(unit.op) {
args.push(oparg);
}
}
args
}
fn varname_index(code: &CodeObject, name: &str) -> usize {
code.varnames
.iter()
.position(|varname| varname.as_str() == name)
.unwrap_or_else(|| panic!("missing {name} local"))
}
fn load_fast_ops_for_var(code: &CodeObject, name: &str) -> Vec<Instruction> {
let var_idx = varname_index(code, name);
non_cache_instructions(code)
.filter_map(|unit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let var_num = var_num.get(OpArg::new(u32::from(u8::from(unit.arg))));
(usize::from(var_num) == var_idx).then_some(unit.op)
}
_ => None,
})
.collect()
}
fn load_fast_pair_ops_for_vars(
code: &CodeObject,
left_name: &str,
right_name: &str,
) -> Vec<Instruction> {
let left_idx = varname_index(code, left_name);
let right_idx = varname_index(code, right_name);
non_cache_instructions(code)
.filter_map(|unit| {
let var_nums = match unit.op {
Instruction::LoadFastLoadFast { var_nums }
| Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => var_nums,
_ => return None,
};
let (left, right) = var_nums
.get(OpArg::new(u32::from(u8::from(unit.arg))))
.indexes();
(usize::from(left) == left_idx && usize::from(right) == right_idx)
.then_some(unit.op)
})
.collect()
}
fn count_strong_loads_for_vars(code: &CodeObject, names: &[&str]) -> usize {
let var_indices = names
.iter()
.map(|name| varname_index(code, name))
.collect::<Vec<_>>();
non_cache_instructions(code)
.filter(|unit| match unit.op {
Instruction::LoadFast { var_num } => {
let var_num = var_num.get(OpArg::new(u32::from(u8::from(unit.arg))));
var_indices.contains(&usize::from(var_num))
}
_ => false,
})
.count()
}
fn count_strong_loads(code: &CodeObject) -> usize {
non_cache_instructions(code)
.filter(|unit| matches!(unit.op, Instruction::LoadFast { .. }))
.count()
}
#[test]
fn match_or_default_block_keeps_load_fast_strong() {
let code = compile_exec(
r#"
def f(format, other):
match format:
case 1 | 2:
return other
case _:
raise NotImplementedError(other)
"#,
);
let function = find_code(&code, "f").expect("missing function code");
let loads = load_fast_ops_for_var(function, "other");
assert!(
matches!(
loads.as_slice(),
[
Instruction::LoadFastBorrow { .. },
Instruction::LoadFastBorrow { .. },
Instruction::LoadFast { .. },
]
),
"CPython optimize_load_fast() keeps trailing OR-pattern default loads strong, got {loads:?}",
);
}
#[test]
fn match_nested_or_default_block_keeps_load_fast_strong() {
let code = compile_exec(
r#"
def f(format, other):
match format:
case [1 | 2, value]:
return other
case _:
raise NotImplementedError(other)
"#,
);
let function = find_code(&code, "f").expect("missing function code");
let loads = load_fast_ops_for_var(function, "other");
assert!(
loads
.iter()
.all(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"CPython 3.14 optimize_load_fast() borrows nested OR-pattern default loads, got {loads:?}",
);
}
#[test]
fn match_success_next_location_preserves_pass_nop() {
let code = compile_exec(
r#"
def f(command):
match command:
case "":
pass
case _ as unknown:
sink(unknown)
return False
"#,
);
let function = find_code(&code, "f").expect("missing function code");
let ops = non_cache_instructions(function)
.map(|unit| unit.op)
.collect::<Vec<_>>();
assert!(
ops.windows(3).any(|window| matches!(
window,
[
Instruction::PopTop,
Instruction::Nop,
Instruction::LoadConst { .. },
]
)),
"CPython NEXT_LOCATION keeps the pass NOP after match subject POP_TOP, got {ops:?}",
);
}
#[test]
fn match_subject_copy_uses_case_pattern_location_like_cpython() {
let code = compile_exec(
"\
def f(x):
match x:
case 1:
return True
case 2:
return False
",
);
let f = find_code(&code, "f").expect("missing f code");
let copy_line = f
.instructions
.iter()
.zip(&f.locations)
.find_map(|(unit, (location, _))| {
let Instruction::Copy { i } = unit.op else {
return None;
};
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
(i.get(arg) == 1).then_some(location.line.get())
})
.expect("missing match subject COPY");
assert_eq!(
copy_line, 3,
"CPython codegen_match_inner() emits ADDOP_I(c, LOC(m->pattern), COPY, 1)"
);
}
#[test]
fn match_or_alternative_copies_use_alternative_locations_like_cpython() {
let code = compile_exec(
"\
def f():
x = False
match 0:
case 0 | 1 | 2 | 3:
x = True
return x
",
);
let f = find_code(&code, "f").expect("missing f code");
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x08, 0x0d, 0x80, 0x41, 0xd8, 0x0a, 0x0b, 0xdf, 0x0d, 0x0e, 0x97,
0x11, 0x97, 0x51, 0x9f, 0x11, 0x88, 0x5d, 0xe0, 0x0b, 0x0c, 0x80, 0x48, 0xf0, 0x05,
0x00, 0x0e, 0x1b, 0xd8, 0x10, 0x14, 0x88, 0x41, 0xd8, 0x0b, 0x0c, 0x80, 0x48,
],
"CPython codegen_pattern_or() emits each alternative COPY with LOC(alt)"
);
}
#[test]
fn match_or_conflicting_bind_error_uses_or_pattern_location_like_cpython() {
let error = compile_exec_error(
"\
def f(x):
match x:
case (
a
| b
):
pass
",
);
let location = error.location.expect("missing error location");
assert_eq!(
location.line.get(),
4,
"CPython codegen_pattern_or() reports alternative binding mismatches at LOC(p), not LOC(alt)"
);
}
#[test]
fn match_or_duplicate_store_error_uses_or_pattern_location_like_cpython() {
let error = compile_exec_error(
"\
def f(value):
match value:
case [
x,
(x | x),
]:
pass
",
);
let location = error.location.expect("missing error location");
assert_eq!(
location.line.get(),
5,
"CPython codegen_pattern_or() reports merge-time duplicate stores at LOC(p)"
);
}
#[test]
fn match_success_jump_uses_no_location_like_cpython() {
let code = compile_exec(
"\
def f(self):
match 0:
case 0:
x = True
case 0:
x = False
self.assertIs(x, True)
",
);
let f = find_code(&code, "f").expect("missing f code");
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x0a, 0x0b, 0xde, 0x0d, 0x0e, 0xd9, 0x10, 0x14, 0x89, 0x41, 0xdd,
0x0d, 0x0e, 0xd8, 0x10, 0x15, 0x88, 0x41, 0xd8, 0x04, 0x08, 0x87, 0x4d, 0x81, 0x4d,
0x90, 0x21, 0x90, 0x54, 0xd6, 0x04, 0x1a,
],
"CPython codegen_match_inner() emits the success jump with NO_LOCATION"
);
}
#[test]
fn match_default_simple_guard_jump_uses_guard_location_like_cpython() {
let code = compile_exec(
"\
def f(x, y):
match x:
case 0:
return 1
case _ if y:
return 2
return 3
",
);
let f = find_code(&code, "f").expect("missing f code");
let guard_jump_location = f
.instructions
.iter()
.enumerate()
.find_map(|(idx, unit)| {
let (Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }) =
unit.op
else {
return None;
};
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
if f.varnames[usize::from(var_num.get(arg))] != "y" {
return None;
}
f.instructions
.iter()
.zip(&f.locations)
.skip(idx + 1)
.take(8)
.find_map(|(unit, (location, _))| {
matches!(unit.op, Instruction::PopJumpIfFalse { .. }).then_some(*location)
})
})
.expect("missing default guard jump");
assert_eq!(
(
guard_jump_location.line.get(),
guard_jump_location.character_offset.get()
),
(5, 19),
"CPython codegen_jump_if() receives LOC(pattern), but the simple guard fallback emits TO_BOOL/jump at LOC(guard)"
);
}
#[test]
fn match_mapping_keys_scaffolding_uses_mapping_location_like_cpython() {
let code = compile_exec(
"\
def f(self):
x = {}
y = None
match x:
case {0: 0}:
y = 0
self.assertIs(y, None)
",
);
let f = find_code(&code, "f").expect("missing f code");
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x08, 0x0a, 0x80, 0x41, 0xd8, 0x08, 0x0c, 0x80, 0x41, 0xd8, 0x0a,
0x0b, 0xdf, 0x0d, 0x13, 0x8f, 0x56, 0x8a, 0x56, 0x95, 0x11, 0x89, 0x56, 0xd8, 0x10,
0x11, 0x89, 0x41, 0xf2, 0x03, 0x00, 0x0e, 0x14, 0xe0, 0x04, 0x08, 0x87, 0x4d, 0x81,
0x4d, 0x90, 0x21, 0x90, 0x54, 0xd6, 0x04, 0x1a,
],
"CPython codegen_pattern_mapping() returns to LOC(p) for BUILD_TUPLE/MATCH_KEYS scaffolding"
);
}
#[test]
fn match_mapping_rest_cleanup_uses_mapping_location_like_cpython() {
let code = compile_exec(
"\
def f(x):
match x:
case {
0: _,
**rest,
}:
return rest
",
);
let f = find_code(&code, "f").expect("missing f code");
let rest_cleanup_start = f
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::BuildMap { .. }))
.expect("missing BUILD_MAP");
for expected in [
"BUILD_MAP",
"DICT_UPDATE",
"DELETE_SUBSCR",
"rest cleanup COPY",
"rest cleanup SWAP",
] {
let location = f
.instructions
.iter()
.zip(&f.locations)
.skip(rest_cleanup_start)
.find_map(|(unit, (location, _))| {
let found = matches!(
(expected, unit.op),
("BUILD_MAP", Instruction::BuildMap { .. })
| ("DICT_UPDATE", Instruction::DictUpdate { .. })
| ("DELETE_SUBSCR", Instruction::DeleteSubscr)
| ("rest cleanup COPY", Instruction::Copy { .. })
| ("rest cleanup SWAP", Instruction::Swap { .. })
);
found.then_some(*location)
})
.unwrap_or_else(|| panic!("missing {expected}"));
assert_eq!(
location.line.get(),
3,
"CPython codegen_pattern_mapping() emits {expected} with LOC(p)"
);
}
}
#[test]
fn match_class_scaffolding_uses_class_pattern_location_like_cpython() {
let code = compile_exec(
"\
def f(x):
match x:
case bool(z):
y = 0
return y, z
",
);
let f = find_code(&code, "f").expect("missing f code");
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x0a, 0x0b, 0xdc, 0x0d, 0x11, 0x8f, 0x57, 0x88, 0x57, 0xd8, 0x10,
0x11, 0x88, 0x41, 0xd8, 0x0b, 0x0c, 0x88, 0x34, 0x80, 0x4b, 0xf0, 0x05, 0x00, 0x0e,
0x15, 0xe0, 0x0b, 0x0c, 0x88, 0x61, 0x88, 0x34, 0x80, 0x4b,
],
"CPython codegen_pattern_class() returns to LOC(p) after VISIT(cls)"
);
}
#[test]
fn match_class_wildcard_pop_uses_class_pattern_location_like_cpython() {
let code = compile_exec(
"\
def f(x):
match x:
case bool(
_
):
return True
",
);
let f = find_code(&code, "f").expect("missing f code");
let unpack_index = f
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::UnpackSequence { .. }))
.expect("missing class pattern UNPACK_SEQUENCE");
let wildcard_pop_location = f
.instructions
.iter()
.zip(&f.locations)
.skip(unpack_index + 1)
.find_map(|(unit, (location, _))| {
matches!(unit.op, Instruction::PopTop).then_some(*location)
})
.expect("missing wildcard POP_TOP");
assert_eq!(
wildcard_pop_location.line.get(),
3,
"CPython codegen_pattern_class() emits wildcard POP_TOP with LOC(p)"
);
}
#[test]
fn while_try_body_layout_keeps_false_jump_to_anchor() {
let code = compile_exec(
r#"
def f(stack, itstack, node_to_stack_index):
while True:
while stack:
try:
node = itstack[-1]()
break
except StopIteration:
del node_to_stack_index[stack.pop()]
itstack.pop()
else:
break
"#,
);
let function = find_code(&code, "f").expect("missing function code");
let ops = non_cache_instructions(function)
.map(|unit| unit.op)
.collect::<Vec<_>>();
let stack_test = ops
.windows(5)
.find(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::Nop,
]
)
})
.unwrap_or_else(|| {
panic!("expected CPython-style while/try false jump to anchor, got {ops:?}")
});
assert!(matches!(stack_test[2], Instruction::PopJumpIfFalse { .. }));
}
#[test]
fn while_if_not_break_keeps_body_call() {
let code = compile_exec(
r#"
def f(waiters):
while waiters:
waiter = waiters.popleft()
if not waiter.done():
waiter.set_result(None)
break
"#,
);
let function = find_code(&code, "f").expect("missing function code");
let ops = non_cache_instructions(function)
.map(|unit| unit.op)
.collect::<Vec<_>>();
assert!(
ops.windows(4).any(|window| matches!(
window,
[
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)),
"CPython keeps waiter.set_result(None) before the break, got {ops:?}",
);
}
fn localsplus_name(code: &CodeObject, idx: usize) -> Option<&str> {
if idx < code.varnames.len() {
return Some(code.varnames[idx].as_str());
}
let mut extra_idx = idx - code.varnames.len();
for cellvar in &code.cellvars {
if !code.varnames.iter().any(|varname| varname == cellvar) {
if extra_idx == 0 {
return Some(cellvar.as_str());
}
extra_idx -= 1;
}
}
code.freevars.get(extra_idx).map(|name| name.as_str())
}
fn has_common_constant(code: &CodeObject, expected: bytecode::CommonConstant) -> bool {
code.instructions.iter().any(|unit| match unit.op {
Instruction::LoadCommonConstant { idx } => {
idx.get(OpArg::new(u32::from(u8::from(unit.arg)))) == expected
}
_ => false,
})
}
fn has_intrinsic_1(code: &CodeObject, expected: IntrinsicFunction1) -> bool {
code.instructions.iter().any(|unit| match unit.op {
Instruction::CallIntrinsic1 { func } => {
func.get(OpArg::new(u32::from(u8::from(unit.arg)))) == expected
}
_ => false,
})
}
#[test]
fn trace_assert_true_try_pair() {
let trace = compile_exec_late_cfg_trace(
"\
try:
assert True
except AssertionError as e:
fail()
try:
assert True, 'msg'
except AssertionError as e:
fail()
",
);
for (stage, dump) in trace {
eprintln!("=== {stage} ===\n{dump}");
}
}
#[test]
fn trace_for_unpack_list_literal() {
let trace = compile_exec_late_cfg_trace(
"\
result = []
for x, in [(1,), (2,), (3,)]:
result.append(x)
",
);
for (stage, dump) in trace {
eprintln!("=== {stage} ===\n{dump}");
}
}
#[test]
fn trace_break_in_finally_function() {
let trace = compile_single_function_late_cfg_trace(
"\
def f(self):
count = 0
while count < 2:
count += 1
try:
pass
finally:
break
self.assertEqual(count, 1)
",
"f",
);
for (stage, dump) in trace {
eprintln!("=== {stage} ===\n{dump}");
}
}
#[test]
fn import_originated_name_disables_method_call_optimization_even_with_local_import() {
let code = compile_exec(
"\
import warnings
def f(ch):
import warnings
warnings.warn(
'\"\\\\%c\" is an invalid escape sequence' % ch
if 0x20 <= ch < 0x7F
else '\"\\\\x%02x\" is an invalid escape sequence' % ch,
DeprecationWarning,
stacklevel=2,
)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f.instructions.iter().map(|unit| unit.op).collect();
let warn_attr = ops
.iter()
.position(|op| matches!(op, Instruction::LoadAttr { .. }))
.expect("missing LOAD_ATTR for warnings.warn");
let push_null = ops[warn_attr + 10..]
.iter()
.position(|op| matches!(op, Instruction::PushNull))
.map(|idx| warn_attr + 10 + idx)
.expect("expected PUSH_NULL after plain LOAD_ATTR");
let load_attr = match f.instructions[warn_attr].op {
Instruction::LoadAttr { namei } => namei.get(OpArg::new(u32::from(u8::from(
f.instructions[warn_attr].arg,
)))),
_ => unreachable!(),
};
assert!(
!load_attr.is_method(),
"import-originated names should use plain LOAD_ATTR"
);
assert!(
matches!(ops[push_null + 1], Instruction::LoadSmallInt { .. }),
"expected warning message expression to start after PUSH_NULL, got ops={ops:?}"
);
}
#[test]
fn trace_constant_false_elif_chain() {
let trace = compile_exec_late_cfg_trace(
"\
if 0: pass
elif 0: pass
elif 0: pass
elif 0: pass
else: pass
",
);
for (stage, dump) in trace {
eprintln!("=== {stage} ===\n{dump}");
}
}
#[test]
fn trace_multi_pass_suite() {
let trace = compile_exec_late_cfg_trace(
"\
if 1:
#
#
#
pass
pass
#
pass
#
",
);
for (stage, dump) in trace {
eprintln!("=== {stage} ===\n{dump}");
}
}
#[test]
fn trace_single_compare_if() {
let trace = compile_exec_late_cfg_trace(
"\
if 1 == 1:
pass
",
);
for (stage, dump) in trace {
eprintln!("=== {stage} ===\n{dump}");
}
}
#[test]
fn trace_comparison_suite() {
let trace = compile_exec_late_cfg_trace(
"\
if 1: pass
x = (1 == 1)
if 1 == 1: pass
if 1 != 1: pass
if 1 < 1: pass
if 1 > 1: pass
if 1 <= 1: pass
if 1 >= 1: pass
if x is x: pass
if x is not x: pass
if 1 in (): pass
if 1 not in (): pass
",
);
for (stage, dump) in trace {
eprintln!("=== {stage} ===\n{dump}");
}
}
#[test]
fn trace_if_for_except_layout() {
let trace = compile_exec_late_cfg_trace(
"\
from sys import maxsize
if maxsize == 2147483647:
for s in ('2147483648', '0o40000000000', '0x100000000', '0b10000000000000000000000000000000'):
try:
x = eval(s)
except OverflowError:
fail(\"OverflowError on huge integer literal %r\" % s)
elif maxsize == 9223372036854775807:
pass
",
);
for (stage, dump) in trace {
eprintln!("=== {stage} ===\n{dump}");
}
}
#[test]
fn break_in_finally_tail_loads_borrow_through_empty_fallthrough_block() {
let code = compile_exec(
"\
def f(self):
count = 0
while count < 2:
count += 1
try:
pass
finally:
break
self.assertEqual(count, 1)
",
);
let code = find_code(&code, "f").unwrap();
let ops: Vec<_> = code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFastBorrow { .. },
Instruction::LoadSmallInt { .. },
Instruction::Call { .. }
]
)
}),
"{:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn plain_constant_bool_op_folds_to_selected_operand() {
let code = compile_exec(
"\
x = 1 or 2 or 3
",
);
let ops: Vec<_> = code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let folded_small_int = code.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadSmallInt { i }
if i.get(OpArg::new(u32::from(u8::from(unit.arg)))) == 1
)
});
let folded_const_one = code
.instructions
.iter()
.find_map(|unit| match unit.op {
Instruction::LoadConst { .. } => code.constants.get(usize::from(u8::from(unit.arg))),
_ => None,
})
.is_some_and(|constant| {
matches!(constant, ConstantData::Integer { value } if *value == BigInt::from(1))
});
assert!(
folded_small_int || folded_const_one,
"expected folded constant 1, got ops={ops:?}"
);
assert!(
!ops.iter().any(|op| {
matches!(
op,
Instruction::Copy { .. }
| Instruction::ToBool
| Instruction::PopJumpIfTrue { .. }
| Instruction::PopJumpIfFalse { .. }
)
}),
"plain constant BoolOp should not leave short-circuit scaffolding, got ops={ops:?}"
);
}
#[test]
fn taken_constant_boolop_load_const_uses_literal_location_like_cpython() {
let code = compile_exec(
"\
def and_false(x):
return False and x
def or_true(x):
return True or x
",
);
let and_false = find_code(&code, "and_false").expect("missing and_false code");
let or_true = find_code(&code, "or_true").expect("missing or_true code");
// CPython 3.14 codegen_boolop() VISITs the selected literal before the
// short-circuit jump is optimized away, so the surviving LOAD_CONST
// keeps the literal range rather than the whole BoolOp range.
assert_eq!(
and_false.linetable.as_ref(),
&[0x80, 0x00, 0xd8, 0x0b, 0x10, 0xd0, 0x04, 0x16]
);
assert_eq!(
or_true.linetable.as_ref(),
&[0x80, 0x00, 0xd8, 0x0b, 0x0f, 0xd0, 0x04, 0x14]
);
}
#[test]
fn assert_false_message_call_uses_assert_location_like_cpython() {
let code = compile_exec(
"\
def f():
assert False, \"x\"
",
);
let f = find_code(&code, "f").expect("missing f code");
// CPython 3.14 codegen_assert() emits LOAD_COMMON_CONSTANT and CALL
// at LOC(assert statement), then RAISE_VARARGS at LOC(test).
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x04, 0x15, 0x90, 0x23, 0xd3, 0x04, 0x15, 0x88, 0x35,
]
);
}
#[test]
fn static_swap_implicit_return_keeps_preswap_store_location_like_cpython() {
let code = compile_exec(
"\
def f(a, b):
a, b = a, b
b, a = a, b
",
);
let f = find_code(&code, "f").expect("missing f code");
// CPython 3.14 flowgraph.c resolves line numbers before
// optimize_basic_block() turns BUILD_TUPLE/UNPACK_SEQUENCE into SWAP
// and apply_static_swaps() reorders the STORE_FAST pair. The
// synthetic return epilogue therefore keeps the pre-swap final store
// location.
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x0b, 0x0c, 0x80, 0x71, 0xd8, 0x0b, 0x0c, 0x82, 0x71,
]
);
}
#[test]
fn unpack_store_pair_jump_uses_second_target_location_like_cpython() {
let code = compile_exec(
"\
def f(value):
if value.startswith('=?'):
try:
token, value = get_encoded_word(value)
except E:
token, value = get_atext(value)
else:
token, value = get_atext(value)
atom.append(token)
",
);
let f = find_code(&code, "f").expect("missing f code");
let jump_position = f
.instructions
.iter()
.zip(&f.locations)
.find_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::JumpForward { .. }).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.expect("missing post-try JUMP_FORWARD");
// CPython 3.14 flowgraph.c turns the second STORE_FAST into a NOP
// during STORE_FAST_STORE_FAST fusion, then NOP removal copies that
// second target location onto the following no-location jump.
assert_eq!(jump_position, (4, 20, 4, 25));
}
#[test]
fn chained_store_pair_jump_keeps_copy_target_location_like_cpython() {
let code = compile_exec(
"\
def f(flag):
if flag:
a = b = True
else:
a = False
b = False
g(a, b)
return a
",
);
let f = find_code(&code, "f").expect("missing f code");
let jump_position = f
.instructions
.windows(2)
.zip(f.locations.windows(2))
.find_map(|(units, locations)| {
matches!(units[0].op, Instruction::StoreFastStoreFast { .. })
.then(|| {
matches!(units[1].op, Instruction::JumpForward { .. }).then_some((
locations[1].0.line.get(),
locations[1].0.character_offset.get(),
locations[1].1.line.get(),
locations[1].1.character_offset.get(),
))
})
.flatten()
})
.expect("missing jump after chained STORE_FAST_STORE_FAST");
// CPython 3.14 flowgraph.c preserves the second chained-assignment
// target location on the jump that skips the else body.
assert_eq!(jump_position, (3, 13, 3, 14));
}
#[test]
fn tuple_store_pair_jump_keeps_fused_store_location_like_cpython() {
let code = compile_exec(
"\
def f(flag, n, exp):
if flag:
n, d = n * 10**exp, 1
else:
d = -exp
g(n, d)
return n
",
);
let f = find_code(&code, "f").expect("missing f code");
let jump_position = f
.instructions
.windows(2)
.zip(f.locations.windows(2))
.find_map(|(units, locations)| {
matches!(units[0].op, Instruction::StoreFastStoreFast { .. })
.then(|| {
matches!(units[1].op, Instruction::JumpForward { .. }).then_some((
locations[1].0.line.get(),
locations[1].0.character_offset.get(),
locations[1].1.line.get(),
locations[1].1.character_offset.get(),
))
})
.flatten()
})
.expect("missing jump after tuple STORE_FAST_STORE_FAST");
// Without COPY before the fused stores, CPython keeps the fused
// STORE_FAST_STORE_FAST location on the following jump.
assert_eq!(jump_position, (3, 12, 3, 13));
}
#[test]
fn genexpr_make_closure_and_call_use_genexpr_location_like_cpython() {
let code = compile_exec(
"\
def f(parameters):
return ((p, type(p)) for p in parameters)
",
);
let f = find_code(&code, "f").expect("missing f code");
let genexpr = find_code(f, "<genexpr>").expect("missing genexpr code");
// CPython 3.14 codegen_comprehension() uses LOC(e) for
// codegen_make_closure(), the outer CALL, and the implicit .0 load
// in codegen_sync_comprehension_generator().
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xd9, 0x0b, 0x2d, 0xa1, 0x2a, 0xd3, 0x0b, 0x2d, 0xd0, 0x04, 0x2d,
]
);
assert_eq!(
genexpr.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xd0, 0x0b, 0x2d, 0xa1, 0x2a, 0x98, 0x51, 0x94, 0x04, 0x90,
0x51, 0x93, 0x07, 0x8d, 0x4c, 0xa3, 0x2a, 0xf9,
]
);
}
#[test]
fn implicit_call_genexpr_range_includes_call_parens_like_cpython() {
let code = compile_exec(
"\
def implicit():
return list(x for x in range(10))
def explicit():
return list((x for x in range(10)))
",
);
let implicit = find_code(&code, "implicit").expect("missing implicit code");
let implicit_gen = find_code(implicit, "<genexpr>").expect("missing implicit genexpr code");
let explicit = find_code(&code, "explicit").expect("missing explicit code");
let explicit_gen = find_code(explicit, "<genexpr>").expect("missing explicit genexpr code");
// CPython's parser gives an unparenthesized sole GeneratorExp call
// argument the call-parenthesized range, and codegen_comprehension()
// uses LOC(e) for MAKE_FUNCTION, the outer CALL, and the implicit .0
// LOAD_FAST. Explicitly parenthesized genexprs already carry their own
// parentheses and must not be widened again.
assert_eq!(
implicit.linetable.as_ref(),
&[
0x80, 0x00, 0xdc, 0x0b, 0x0f, 0xd1, 0x0f, 0x25, 0x9c, 0x35, 0xa0, 0x12, 0x9c, 0x39,
0xd3, 0x0f, 0x25, 0xd3, 0x0b, 0x25, 0xd0, 0x04, 0x25,
]
);
assert_eq!(
implicit_gen.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xd0, 0x0f, 0x25, 0x99, 0x39, 0x90, 0x61, 0x94, 0x01, 0x9b,
0x39, 0xf9,
]
);
assert_eq!(
explicit_gen.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xd0, 0x10, 0x26, 0x99, 0x49, 0x90, 0x71, 0x94, 0x11, 0x9b,
0x49, 0xf9,
]
);
}
#[test]
fn implicit_call_genexpr_parenthesized_element_range_like_cpython() {
let code = compile_exec(
"\
def bytes_binop():
return bytes((x ^ 0x5C) for x in range(256))
def dict_tuple(d):
return dict((v, k) for (k, v) in d.items())
def plain_tuple_elt(xs):
return list((x, y) for x, y in xs)
def explicit_gen(xs):
return list(((x, y) for x, y in xs))
",
);
let bytes_binop = find_code(&code, "bytes_binop").expect("missing bytes_binop code");
let bytes_gen = find_code(bytes_binop, "<genexpr>").expect("missing bytes genexpr code");
let dict_tuple = find_code(&code, "dict_tuple").expect("missing dict_tuple code");
let dict_gen = find_code(dict_tuple, "<genexpr>").expect("missing dict genexpr code");
let plain_tuple_elt =
find_code(&code, "plain_tuple_elt").expect("missing plain_tuple_elt code");
let plain_gen =
find_code(plain_tuple_elt, "<genexpr>").expect("missing plain genexpr code");
let explicit_gen = find_code(&code, "explicit_gen").expect("missing explicit_gen code");
let explicit_inner =
find_code(explicit_gen, "<genexpr>").expect("missing explicit genexpr code");
// CPython 3.14's parser includes the call argument parentheses in
// LOC(GeneratorExp) for implicit sole-argument generator expressions,
// even when the element expression itself starts with parentheses.
assert_eq!(
bytes_binop.linetable.as_ref(),
&[
0x80, 0x00, 0xdc, 0x0b, 0x10, 0xd1, 0x10, 0x30, 0xa4, 0x55, 0xa8, 0x33, 0xa4, 0x5a,
0xd3, 0x10, 0x30, 0xd3, 0x0b, 0x30, 0xd0, 0x04, 0x30,
]
);
assert_eq!(
bytes_gen.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xd0, 0x10, 0x30, 0xa1, 0x5a, 0xa0, 0x01, 0x90, 0x64, 0x97,
0x28, 0x92, 0x28, 0xa3, 0x5a, 0xf9,
]
);
assert_eq!(
dict_tuple.linetable.as_ref(),
&[
0x80, 0x00, 0xdc, 0x0b, 0x0f, 0xd1, 0x0f, 0x2f, 0xa0, 0x51, 0xa7, 0x57, 0xa1, 0x57,
0xa4, 0x59, 0xd3, 0x0f, 0x2f, 0xd3, 0x0b, 0x2f, 0xd0, 0x04, 0x2f,
]
);
assert_eq!(
dict_gen.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xd0, 0x0f, 0x2f, 0xa1, 0x59, 0x99, 0x36, 0x98, 0x41, 0x90,
0x11, 0x95, 0x06, 0xa3, 0x59, 0xf9,
]
);
assert_eq!(
plain_tuple_elt.linetable.as_ref(),
&[
0x80, 0x00, 0xdc, 0x0b, 0x0f, 0xd1, 0x0f, 0x26, 0xa1, 0x32, 0xd3, 0x0f, 0x26, 0xd3,
0x0b, 0x26, 0xd0, 0x04, 0x26,
]
);
assert_eq!(
plain_gen.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xd0, 0x0f, 0x26, 0xa1, 0x32, 0x99, 0x34, 0x98, 0x31, 0x90,
0x11, 0x95, 0x06, 0xa3, 0x32, 0xf9,
]
);
assert_eq!(
explicit_gen.linetable.as_ref(),
&[
0x80, 0x00, 0xdc, 0x0b, 0x0f, 0xd1, 0x10, 0x27, 0xa1, 0x42, 0xd3, 0x10, 0x27, 0xd3,
0x0b, 0x28, 0xd0, 0x04, 0x28,
]
);
assert_eq!(
explicit_inner.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xd0, 0x10, 0x27, 0xa1, 0x42, 0x99, 0x44, 0x98, 0x41, 0x90,
0x21, 0x95, 0x16, 0xa3, 0x42, 0xf9,
]
);
}
#[test]
fn implicit_call_genexpr_operator_element_range_like_cpython() {
// The element opens with a parenthesized group that the rest of the
// expression continues, so the call's own parentheses are the only
// ones that bound the generator.
let code = compile_exec(
"\
def f(p, q):
return sum((px - qx) ** 2.0 for px, qx in zip(p, q))
",
);
let f = find_code(&code, "f").expect("missing f code");
let genexpr = find_code(f, "<genexpr>").expect("missing genexpr code");
// Columns are one-based here, so these are `dis`'s 14..56.
assert_eq!(
instruction_range(f, |op| matches!(op, Instruction::MakeFunction)),
Some((2, 15, 2, 57))
);
assert_eq!(
instruction_range(genexpr, |op| matches!(op, Instruction::LoadFast { .. })),
Some((2, 15, 2, 57))
);
}
#[test]
fn fstring_concatenation_without_content_uses_whole_range_like_cpython() {
// Every fragment is empty, so none of them is kept and the empty string
// that replaces them belongs to the whole concatenation. The last line
// keeps its one fragment and stays at that fragment.
let code = compile_exec(
"\
x = '' f''
y = f'' ''
z = '' f'' '' f''
w = f'' 'a' f''
",
);
let ranges: Vec<_> = code
.instructions
.iter()
.zip(&code.locations)
.filter(|(unit, _)| matches!(unit.op, Instruction::LoadConst { .. }))
.map(|(_, locations)| location_range(locations))
.take(4)
.collect();
// Columns are one-based here, so these are `dis`'s 4..10, 4..10,
// 4..17 and 8..11.
assert_eq!(
ranges,
vec![(1, 5, 1, 11), (2, 5, 2, 11), (3, 5, 3, 18), (4, 9, 4, 12)]
);
}
#[test]
fn genexpr_filter_cleanup_jumps_use_element_location_like_cpython() {
let code = compile_exec(
"\
def simple(names):
return (x for x in names if not _ishidden(x))
def boolop(fields):
return (f for f in fields if f.init and not f.kw_only)
",
);
let simple = find_code(&code, "simple").expect("missing simple code");
let simple_gen = find_code(simple, "<genexpr>").expect("missing simple genexpr code");
let boolop = find_code(&code, "boolop").expect("missing boolop code");
let boolop_gen = find_code(boolop, "<genexpr>").expect("missing boolop genexpr code");
// codegen_sync_comprehension_generator() emits the
// comprehension guard jump to if_cleanup, then emits the if_cleanup
// backedge with elt_loc. flowgraph.c::jump_thread() copies that target
// jump location to the threaded POP_JUMP/NOT_TAKEN cleanup path.
assert_eq!(
simple_gen.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xd0, 0x0b, 0x31, 0x91, 0x75, 0x90, 0x21, 0xa4, 0x49, 0xa8,
0x61, 0xa7, 0x4c, 0x8f, 0x41, 0x8a, 0x41, 0x93, 0x75, 0xf9,
]
);
assert_eq!(
boolop_gen.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xd0, 0x0b, 0x3a, 0x91, 0x76, 0x90, 0x21, 0xa7, 0x16, 0xa5,
0x16, 0x8c, 0x41, 0xb0, 0x01, 0xb7, 0x09, 0xb5, 0x09, 0x8f, 0x41, 0x8a, 0x41, 0x93,
0x76, 0xf9,
]
);
}
#[test]
fn try_finally_exception_scaffolding_uses_no_location_like_cpython() {
let code = compile_exec(
"\
def f(self, node):
self.flag = True
try:
self.body(node)
finally:
self.flag = False
",
);
let f = find_code(&code, "f").expect("missing f code");
// codegen_try_finally() emits the exception path
// SETUP_CLEANUP/PUSH_EXC_INFO and POP_EXCEPT_AND_RERAISE with
// NO_LOCATION; flowgraph line propagation then gives only the
// finalbody's direct RERAISE the finalbody location.
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x10, 0x14, 0x80, 0x44, 0x84, 0x49, 0xf0, 0x02, 0x03, 0x05, 0x1a,
0xd8, 0x08, 0x0c, 0x8f, 0x09, 0x89, 0x09, 0x90, 0x24, 0x8c, 0x0f, 0xe0, 0x14, 0x19,
0x88, 0x04, 0x8e, 0x09, 0xf8, 0x90, 0x45, 0x88, 0x04, 0x8d, 0x09, 0xfa,
]
);
}
#[test]
fn return_debug_in_finally_uses_cpython_preprocessed_constant_order() {
let code = compile_exec(
"\
def f(close):
try:
return __debug__
finally:
close()
",
);
let f = find_code(&code, "f").expect("missing f code");
let call_pos = f
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::Call { .. }))
.expect("missing finally-body call");
let debug_load_pos = f
.instructions
.iter()
.position(|unit| {
let Instruction::LoadConst { consti } = unit.op else {
return false;
};
let constant = &f.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))];
matches!(constant, ConstantData::Boolean { value: true })
})
.expect("missing __debug__ constant load");
assert!(
call_pos < debug_load_pos,
"CPython ast_preprocess.c folds __debug__ to Constant before codegen_return(), so the return constant is loaded after finally cleanup; ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn debug_statement_is_preprocessed_constant_like_cpython() {
for code in [
compile_exec("__debug__\n"),
compile_exec_optimized("__debug__\n"),
] {
let ops = non_cache_instructions(&code)
.map(|unit| unit.op)
.collect::<Vec<_>>();
assert!(
!ops.iter().any(|op| matches!(op, Instruction::PopTop)),
"CPython ast_preprocess.c folds __debug__ to Constant before codegen_stmt_expr(), so it must not compile as LOAD_CONST/POP_TOP; ops={ops:?}"
);
}
}
#[test]
fn statement_expr_pop_top_uses_no_location_like_cpython() {
let infos = compile_module_instruction_infos("x + 1\n", Mode::Exec);
let pop = infos
.iter()
.find(|info| matches!(info.instr.real(), Some(Instruction::PopTop)))
.expect("missing expression-statement POP_TOP");
assert_eq!(
pop.lineno_override,
Some(ir::NO_LOCATION_OVERRIDE),
"CPython codegen_stmt_expr() emits artificial expression-statement POP_TOP at NO_LOCATION"
);
}
#[test]
fn interactive_statement_expr_pop_top_uses_no_location_like_cpython() {
let infos = compile_module_instruction_infos("x + 1\n", Mode::Single);
let print = infos
.iter()
.position(|info| {
matches!(
info.instr.real(),
Some(Instruction::CallIntrinsic1 { func })
if func.get(info.arg) == bytecode::IntrinsicFunction1::Print
)
})
.expect("missing interactive PRINT intrinsic");
let pop = infos
.get(print + 1)
.expect("missing POP_TOP after interactive PRINT");
assert!(
matches!(pop.instr.real(), Some(Instruction::PopTop)),
"CPython codegen_stmt_expr() emits POP_TOP immediately after INTRINSIC_PRINT; got {pop:?}"
);
assert_eq!(
pop.lineno_override,
Some(ir::NO_LOCATION_OVERRIDE),
"CPython codegen_stmt_expr() emits interactive PRINT cleanup POP_TOP at NO_LOCATION"
);
}
#[test]
fn import_star_pop_top_uses_no_location_like_cpython() {
let infos = compile_module_instruction_infos("from m import *\n", Mode::Exec);
let import_star = infos
.iter()
.position(|info| {
matches!(
info.instr.real(),
Some(Instruction::CallIntrinsic1 { func })
if func.get(info.arg) == bytecode::IntrinsicFunction1::ImportStar
)
})
.expect("missing IMPORT_STAR intrinsic");
let pop = infos
.get(import_star + 1)
.expect("missing POP_TOP after IMPORT_STAR");
assert!(
matches!(pop.instr.real(), Some(Instruction::PopTop)),
"CPython codegen_from_import() emits POP_TOP immediately after INTRINSIC_IMPORT_STAR; got {pop:?}"
);
assert_eq!(
pop.lineno_override,
Some(ir::NO_LOCATION_OVERRIDE),
"CPython codegen_from_import() emits import-star cleanup POP_TOP at NO_LOCATION"
);
}
#[test]
fn adjacent_no_location_entries_merge_like_cpython() {
let code = compile_exec(
"\
def f(file):
if sys.platform == \"win32\":
try:
import nt
if not nt._supports_virtual_terminal():
return False
except (ImportError, AttributeError):
return False
try:
return os.isatty(file.fileno())
except OSError:
return hasattr(file, \"isatty\") and file.isatty()
",
);
let f = find_code(&code, "f").expect("missing f code");
// CPython's NO_LOCATION is {-1, -1, -1, -1}, and
// assemble.c::assemble_location_info() merges adjacent instructions
// with the same NO_LOCATION into one linetable entry.
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xdc, 0x07, 0x0a, 0x87, 0x7c, 0x81, 0x7c, 0x90, 0x77, 0xd4, 0x07, 0x1e,
0xf0, 0x02, 0x05, 0x09, 0x19, 0xdb, 0x0c, 0x15, 0xd8, 0x13, 0x15, 0xd7, 0x13, 0x30,
0xd1, 0x13, 0x30, 0xd7, 0x13, 0x32, 0xd2, 0x13, 0x32, 0xd9, 0x17, 0x1c, 0xf0, 0x03,
0x00, 0x14, 0x33, 0xf0, 0x08, 0x03, 0x05, 0x39, 0xdc, 0x0f, 0x11, 0x8f, 0x79, 0x89,
0x79, 0x98, 0x14, 0x9f, 0x1b, 0x99, 0x1b, 0x9b, 0x1d, 0xd3, 0x0f, 0x27, 0xd0, 0x08,
0x27, 0xf8, 0xf4, 0x07, 0x00, 0x11, 0x1c, 0x9c, 0x5e, 0xd0, 0x0f, 0x2c, 0xf4, 0x00,
0x01, 0x09, 0x19, 0xda, 0x13, 0x18, 0xf0, 0x03, 0x01, 0x09, 0x19, 0xfb, 0xf4, 0x08,
0x00, 0x0c, 0x13, 0xf4, 0x00, 0x01, 0x05, 0x39, 0xdc, 0x0f, 0x16, 0x90, 0x74, 0x98,
0x58, 0xd3, 0x0f, 0x26, 0xd7, 0x0f, 0x38, 0xd0, 0x0f, 0x38, 0xa8, 0x34, 0xaf, 0x3b,
0xa9, 0x3b, 0xab, 0x3d, 0xd2, 0x08, 0x38, 0xf0, 0x03, 0x01, 0x05, 0x39, 0xfa,
]
);
}
#[test]
fn fstring_format_ops_use_formatted_value_location_like_cpython() {
let code = compile_exec(
"\
def simple(self):
return f'{self.value}'
def spec(x):
return f'{x!r:>3}'
",
);
let simple = find_code(&code, "simple").expect("missing simple code");
let spec = find_code(&code, "spec").expect("missing spec code");
// CPython 3.14 codegen_formatted_value() VISITs the inner expression
// first, then emits CONVERT_VALUE / FORMAT_* at LOC(FormattedValue).
assert_eq!(
simple.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x0e, 0x12, 0x8f, 0x6a, 0x89, 0x6a, 0x88, 0x5c, 0xd0, 0x04, 0x1a,
]
);
assert_eq!(
spec.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x0e, 0x0f, 0x88, 0x58, 0x90, 0x22, 0x88, 0x58, 0xd0, 0x04, 0x16,
]
);
}
#[test]
fn debug_fstring_literal_merging_and_location_like_cpython() {
fn string_load_position(code: &CodeObject, expected: &str) -> (usize, usize, usize, usize) {
code.instructions
.iter()
.zip(&code.locations)
.find_map(|(unit, (location, end_location))| {
let Instruction::LoadConst { consti } = unit.op else {
return None;
};
let constant =
&code.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))];
matches!(constant, ConstantData::Str { value } if value.to_string() == expected)
.then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.expect("missing debug f-string literal")
}
let code = compile_exec(
r#"def simple(x):
return f'{x=}'
def prefixed(x):
return f'a {x=} b'
def commented(x):
return f"""{ # comment
x=}"""
"#,
);
let simple = find_code(&code, "simple").expect("missing simple code");
let prefixed = find_code(&code, "prefixed").expect("missing prefixed code");
let commented = find_code(&code, "commented").expect("missing commented code");
assert_eq!(
string_load_position(simple, "x="),
(2, 15, 2, 17),
"CPython represents f'{{x=}}' debug text as a literal at the expression/debug-text location"
);
assert_eq!(
string_load_position(prefixed, "a x="),
(5, 14, 5, 19),
"CPython merges debug text with the preceding JoinedStr literal"
);
assert_eq!(
string_load_position(commented, " \nx="),
(8, 17, 9, 3),
"a stripped comment shortens the debug text but not the source range it spans"
);
}
#[test]
fn fstring_format_spec_build_string_location_like_cpython() {
let code = compile_exec(
"\
def simple(lbl, label_width):
return f'{lbl:>{label_width}}'
def padded(digits, int_len):
return f'{digits:0>{int_len + 1}d}'
",
);
let simple = find_code(&code, "simple").expect("missing simple code");
let padded = find_code(&code, "padded").expect("missing padded code");
let build_string_position = |code: &CodeObject| {
code.instructions
.iter()
.zip(&code.locations)
.find_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::BuildString { .. }).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.expect("missing format-spec BUILD_STRING")
};
assert_eq!(
build_string_position(simple),
(2, 18, 2, 33),
"CPython uses the format-spec JoinedStr location, including the ':' prefix, for BUILD_STRING"
);
assert_eq!(
build_string_position(padded),
(5, 21, 5, 38),
"CPython format-spec JoinedStr location spans from ':' through the final literal"
);
}
#[test]
fn joined_string_literals_extend_pending_literal_location_like_cpython() {
let code = compile_exec(
"\
def f(a):
return (
'x'
f'y{a}z'
'w'
)
",
);
let f = find_code(&code, "f").expect("missing f code");
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xf0, 0x04, 0x01, 0x09, 0x0c, 0xd8, 0x0c, 0x0d, 0x88, 0x33, 0xf0, 0x00,
0x01, 0x0f, 0x0c, 0xf0, 0x03, 0x02, 0x09, 0x0c, 0xf0, 0x03, 0x04, 0x05, 0x06,
],
"CPython parser/codegen represents adjacent f-string literal fragments as Constant ranges spanning the merged fragments"
);
}
#[test]
fn starred_call_preserves_bool_op_short_circuit_shape() {
let code = compile_exec(
"\
def f(g):
return g(*(() or (1,)))
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.iter().any(|op| matches!(op, Instruction::Copy { .. })),
"starred BoolOp should keep short-circuit COPY, got ops={ops:?}"
);
assert!(
ops.iter().any(|op| matches!(op, Instruction::ToBool)),
"starred BoolOp should keep TO_BOOL, got ops={ops:?}"
);
assert!(
ops.iter()
.any(|op| matches!(op, Instruction::PopJumpIfTrue { .. })),
"starred BoolOp should keep POP_JUMP_IF_TRUE, got ops={ops:?}"
);
}
#[test]
fn partial_constant_bool_op_folds_prefix_in_value_context() {
let code = compile_exec(
"\
def outer(null):
@False or null
def f(x):
pass
",
);
let outer = find_code(&code, "outer").expect("missing outer code");
let ops: Vec<_> = outer
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.iter().any(|op| {
matches!(
op,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }
)
}),
"expected surviving decorator expression to load null directly, got ops={ops:?}"
);
assert!(
!ops.iter().any(|op| {
matches!(
op,
Instruction::Copy { .. }
| Instruction::ToBool
| Instruction::PopJumpIfTrue { .. }
| Instruction::PopJumpIfFalse { .. }
)
}),
"partial constant BoolOp should not leave short-circuit scaffolding, got ops={ops:?}"
);
}
#[test]
fn decorated_definitions_use_cpython_locations() {
let code = compile_exec(
"\
def dec(f): return f
class C:
@dec
def f(self):
yield
@dec
class D:
pass
class E:
@dec
def g(self, flags: int, /) -> memoryview:
raise NotImplementedError
",
);
let c = find_code(&code, "C").expect("missing C code");
let d = find_code(&code, "D").expect("missing D code");
let e = find_code(&code, "E").expect("missing E code");
let annotate = find_code(e, "__annotate__").expect("missing annotation code");
// CPython 3.14 codegen_function()/codegen_class() evaluate
// decorators first, then use LOC(s) for codegen_make_closure() and
// codegen_nameop(); codegen_apply_decorators() emits CALL at each
// decorator expression's location.
assert_eq!(
c.linetable.as_ref(),
&[
0xf8, 0x87, 0x00, 0x80, 0x00, 0xd8, 0x05, 0x08, 0xf1, 0x02, 0x01, 0x05, 0x0e, 0xf3,
0x03, 0x00, 0x06, 0x09, 0xf6, 0x02, 0x01, 0x05, 0x0e,
]
);
assert_eq!(d.linetable.as_ref(), &[0x86, 0x00, 0xe3, 0x04, 0x08]);
assert_eq!(
e.linetable.as_ref(),
&[
0xf8, 0x87, 0x00, 0x80, 0x00, 0xd8, 0x05, 0x08, 0xf7, 0x02, 0x01, 0x05, 0x22, 0xf3,
0x03, 0x00, 0x06, 0x09, 0xf6, 0x02, 0x01, 0x05, 0x22,
]
);
assert_eq!(
annotate.linetable.as_ref(),
&[
0xf8, 0x80, 0x00, 0xf7, 0x00, 0x01, 0x05, 0x22, 0xf1, 0x00, 0x01, 0x05, 0x22, 0x91,
0x73, 0xf0, 0x00, 0x01, 0x05, 0x22, 0xa1, 0x2a, 0xf1, 0x00, 0x01, 0x05, 0x22,
]
);
}
#[test]
fn taken_constant_boolop_jump_disables_following_borrows() {
for source in [
"\
def f(self):
if 0 and self.h:
self.x = self.y
elif self.a and self.b:
self.x = self.y
self.z = self.w
",
"\
def f(self):
if 1 or self.h:
self.x = self.y
self.z = self.w
",
] {
let code = compile_exec(source);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython keeps plain LOAD_FAST after an always-taken constant bool-op jump, got ops={ops:?}"
);
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"always-taken constant bool-op jump should suppress later LOAD_FAST_BORROW, got ops={ops:?}"
);
}
}
#[test]
fn not_taken_constant_boolop_jump_keeps_following_borrows() {
for source in [
"\
def f(self):
if 1 and self.h:
self.x = self.y
self.z = self.w
",
"\
def f(self):
if 0 or self.h:
self.x = self.y
self.z = self.w
",
] {
let code = compile_exec(source);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.iter()
.any(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"constant bool-op jump that is not taken should keep CPython-style borrows, got ops={ops:?}"
);
}
}
#[test]
fn while_before_folded_boolop_if_keeps_successor_load_fast_strong() {
let code = compile_exec(
"\
def f(running, errors):
try:
while running:
pass
if __debug__ and errors:
raise ExceptionGroup('x', errors)
return errors
finally:
del errors
",
);
let f = find_code(&code, "f").expect("missing function code");
let errors_loads = load_fast_ops_for_var(f, "errors");
assert!(
errors_loads
.iter()
.all(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython codegen_while() emits USE_LABEL(end), then codegen_jump_if() emits a folded constant BoolOp prefix; flowgraph.c::basicblock_optimize_load_const() and optimize_load_fast() leave the successor errors loads strong, got {errors_loads:?}"
);
}
#[test]
fn with_try_except_tail_keeps_successor_load_fast_strong() {
let code = compile_exec(
"\
def f(self, cm, value):
with cm:
try:
self.run()
except OSError as e:
if e.errno:
raise ConnectionError
else:
raise
self.run_loop(value.done)
self.assertTrue(value.nbytes)
",
);
let f = find_code(&code, "f").expect("missing function code");
let value_loads = load_fast_ops_for_var(f, "value");
assert!(
value_loads
.iter()
.all(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython codegen_try_except() leaves USE_LABEL(end) before codegen_with_inner() emits normal __exit__ cleanup, so optimize_load_fast() leaves with-successor loads strong; got {value_loads:?}"
);
}
#[test]
fn with_try_except_conditional_body_allows_successor_borrow() {
let code = compile_exec(
"\
def f(max_decode, gzf, cm, decoded):
with cm:
try:
if max_decode < 0:
decoded = gzf.read()
else:
decoded = gzf.read(max_decode + 1)
except OSError:
raise ValueError('invalid data')
if max_decode >= 0 and len(decoded) > max_decode:
raise ValueError('too large')
return decoded
",
);
let f = find_code(&code, "f").expect("missing f code");
let max_decode_loads = load_fast_ops_for_var(f, "max_decode");
assert!(
max_decode_loads
.iter()
.all(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"CPython codegen_try_except() keeps borrowing through a with tail when the protected try body ends with a conditional label, got {max_decode_loads:?}"
);
}
#[test]
fn loop_try_orelse_nested_try_before_next_try_keeps_load_fast_strong() {
let code = compile_exec(
"\
def f(test_cases):
for src, dest in test_cases:
try:
os.symlink(src, dest)
except FileNotFoundError:
pass
else:
try:
os.remove(dest)
except OSError:
pass
try:
os.symlink(os.fsencode(src), os.fsencode(dest))
except FileNotFoundError:
pass
else:
try:
os.remove(dest)
except OSError:
pass
",
);
let f = find_code(&code, "f").expect("missing function code");
let strong_src_dest_loads = count_strong_loads_for_vars(f, &["src", "dest"]);
assert!(
strong_src_dest_loads >= 2,
"CPython codegen_try_except() emits orelse then USE_LABEL(end) before the following loop try, so optimize_load_fast() leaves fsencode arguments strong; got {strong_src_dest_loads} strong src/dest loads"
);
}
#[test]
fn try_orelse_with_before_next_try_keeps_load_fast_strong() {
let code = compile_exec(
"\
def f(self, f, cm):
try:
f = C(0)
except ValueError:
pass
else:
self.assertTrue(f.readable())
with cm:
with C(False):
pass
try:
f = C(1)
except ValueError:
pass
else:
self.assertFalse(f.readable())
",
);
let f_code = find_code(&code, "f").expect("missing function code");
let strong_self_or_f_loads = count_strong_loads_for_vars(f_code, &["self", "f"]);
assert!(
strong_self_or_f_loads >= 2,
"CPython codegen_try_except() emits orelse with nested with cleanup before USE_LABEL(end), so optimize_load_fast() leaves loads in the following try/else strong; got {strong_self_or_f_loads} strong self/f loads"
);
}
#[test]
fn try_orelse_single_with_before_next_try_keeps_borrows() {
let code = compile_exec(
"\
def f(self, cm):
try:
import _testcapi
except ImportError:
pass
else:
code = 'x'
with cm:
out = self.run_xdev('-c', code)
self.assertEqual(out, 'x')
try:
import faulthandler
except ImportError:
pass
else:
code = 'y'
out = self.run_xdev('-c', code)
self.assertEqual(out, 'y')
",
);
let f = find_code(&code, "f").expect("missing function code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let final_return = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::ReturnValue))
.expect("missing return");
let tail = &instructions[..final_return];
let borrowed_self_loads = tail
.iter()
.filter(|unit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == "self"
}
_ => false,
})
.count();
assert!(
borrowed_self_loads >= 4,
"CPython codegen_with() for a single with in try/else does not make the following try/else a load-fast barrier; expected borrowed self loads, got instructions={instructions:?}"
);
}
#[test]
fn with_try_finally_nested_with_keeps_successor_load_fast_strong() {
let code = compile_exec(
"\
def f(self, cm):
with cm as cm1:
try:
work()
finally:
with cm as cm2:
work()
e1 = cm1.exception
e12 = e1.__cause__
self.assertIsInstance(e12, Error)
",
);
let f = find_code(&code, "f").expect("missing function code");
let strong_loads = count_strong_loads(f);
assert!(
strong_loads >= 3,
"CPython codegen_try_finally() inlines a finalbody with codegen_with_inner() cleanup before the with exit label, so optimize_load_fast() leaves successor loads strong; got {strong_loads} strong loads"
);
}
#[test]
fn nonliteral_constant_bool_op_preserves_short_circuit_shape() {
let code = compile_exec(
"\
x = (\"a\"[0]) or 2
",
);
let ops: Vec<_> = code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!code.instructions.iter().any(|unit| matches!(
unit.op,
Instruction::BinaryOp { op }
if op.get(OpArg::new(u32::from(u8::from(unit.arg))))
== oparg::BinaryOperator::Subscr
)),
"constant subscript should fold before bool-op lowering, got ops={ops:?}"
);
assert!(
ops.iter().any(|op| matches!(op, Instruction::Copy { .. })),
"folded non-literal BoolOp operand should keep COPY, got ops={ops:?}"
);
assert!(
ops.iter().any(|op| matches!(op, Instruction::ToBool)),
"folded non-literal BoolOp operand should keep TO_BOOL, got ops={ops:?}"
);
assert!(
ops.iter()
.any(|op| matches!(op, Instruction::PopJumpIfTrue { .. })),
"folded non-literal BoolOp operand should keep POP_JUMP_IF_TRUE, got ops={ops:?}"
);
}
#[test]
fn unary_positive_complex_constant_folds_to_load_const() {
let code = compile_exec(
"\
x = +0.0j
",
);
let ops: Vec<_> = code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::CallIntrinsic1 { .. })),
"unary positive complex constant should not leave CALL_INTRINSIC_1, got ops={ops:?}"
);
assert!(
matches!(
ops.as_slice(),
[
Instruction::Resume { .. },
Instruction::LoadConst { .. },
Instruction::StoreName { .. },
Instruction::LoadConst { .. },
Instruction::ReturnValue
]
),
"expected module assignment to fold +0.0j into LOAD_CONST, got ops={ops:?}"
);
}
#[test]
fn folded_nonliteral_bool_op_tail_keeps_plain_load_fast() {
let code = compile_exec(
"\
def and_true(x):
return True and x
def or_false(x):
return False or x
",
);
for name in ["and_true", "or_false"] {
let function = find_code(&code, name).unwrap_or_else(|| panic!("missing {name} code"));
let ops: Vec<_> = function
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"expected folded bool-op tail to keep LOAD_FAST in {name}, got ops={ops:?}"
);
assert!(
!ops.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"folded bool-op tail should not introduce borrow loads in {name}, got ops={ops:?}"
);
}
}
#[test]
fn folded_nonliteral_bool_op_direct_tail_load_keeps_plain_load_fast() {
let code = compile_exec(
"\
def return_tail(x):
return False or x
def assign_tail(x):
y = False or x
return y
def call_arg(x, g):
return g(False or x)
def attr_tail(x):
return False or x.y
def class_tail(class_decorator):
@False or class_decorator
class H:
pass
",
);
for name in ["return_tail", "assign_tail", "call_arg", "class_tail"] {
let function = find_code(&code, name).unwrap_or_else(|| panic!("missing {name} code"));
let local = if name == "class_tail" {
"class_decorator"
} else {
"x"
};
let loads = load_fast_ops_for_var(function, local);
assert!(
loads
.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython codegen_boolop() emits USE_LABEL(end) after the folded BoolOp tail, so optimize_load_fast() leaves the direct tail load strong in {name}; got {loads:?}"
);
}
let attr_tail = find_code(&code, "attr_tail").expect("missing attr_tail code");
let attr_receiver_loads = load_fast_ops_for_var(attr_tail, "x");
assert!(
attr_receiver_loads
.iter()
.all(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"CPython only keeps direct folded tail local loads strong; an attribute receiver is consumed before the BoolOp end label, got {attr_receiver_loads:?}"
);
}
#[test]
fn folded_nonliteral_tuple_unpack_tail_keeps_plain_load_fast() {
let code = compile_exec(
"\
def f(self, mod):
optimize, opt = (1, 1) if __debug__ else (0, '')
mod.call(self.path, optimize=optimize)
cached = mod.cache(self.source_path, optimization=opt)
self.assertTrue(cached)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let tail_start = ops
.iter()
.position(|op| matches!(op, Instruction::LoadFast { .. }))
.expect("missing folded assignment tail load");
let tail = &ops[tail_start..];
assert!(
tail.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"expected folded nonliteral tuple-unpack tail to use strong LOAD_FAST, got tail={tail:?}"
);
assert!(
!tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"folded nonliteral tuple-unpack tail should not borrow local loads, got tail={tail:?}"
);
}
#[test]
fn scope_exit_instructions_keep_line_numbers() {
let code = compile_exec(
"\
async def test():
for stop_exc in (StopIteration('spam'), StopAsyncIteration('ham')):
with self.subTest(type=type(stop_exc)):
try:
async with egg():
raise stop_exc
except Exception as ex:
self.assertIs(ex, stop_exc)
else:
self.fail(f'{stop_exc} was suppressed')
",
);
assert_scope_exit_locations(&code);
}
#[test]
fn attribute_ex_call_uses_plain_load_attr() {
let code = compile_exec(
"\
def f(cls, args, kwargs):
cls.__new__(cls, *args)
cls.__new__(cls, *args, **kwargs)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ex_call_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::CallFunctionEx))
.count();
let load_attr_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::LoadAttr { .. }))
.count();
assert_eq!(ex_call_count, 2);
assert_eq!(load_attr_count, 2);
for unit in f.instructions.iter() {
if let Instruction::LoadAttr { namei } = unit.op {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
assert!(
!load_attr.is_method(),
"CALL_FUNCTION_EX should use plain LOAD_ATTR"
);
}
}
}
#[test]
fn method_call_at_stack_guideline_uses_plain_load_attr_like_cpython() {
let params = (0..STACK_USE_GUIDELINE)
.map(|i| format!("a{i}"))
.collect::<Vec<_>>()
.join(", ");
let code = compile_exec(&format!(
"def f(obj, {params}):\n return obj.m({params})\n"
));
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let plain_load_attr = f.instructions.iter().any(|unit| {
if let Instruction::LoadAttr { namei } = unit.op {
!namei
.get(OpArg::new(u32::from(u8::from(unit.arg))))
.is_method()
} else {
false
}
});
let direct_call_30 = f.instructions.iter().any(|unit| match unit.op {
Instruction::Call { argc } => {
argc.get(OpArg::new(u32::from(u8::from(unit.arg)))) == STACK_USE_GUIDELINE
}
_ => false,
});
assert!(
plain_load_attr && direct_call_30,
"CPython maybe_optimize_method_call rejects arg count at the guideline, got ops={ops:?}"
);
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::CallFunctionEx)),
"exactly guideline-sized method call should stay direct after LOAD_ATTR fallback, got ops={ops:?}"
);
}
#[test]
fn method_call_many_keywords_stays_load_method_call_kw_like_cpython() {
let params = (0..16)
.map(|i| format!("a{i}"))
.collect::<Vec<_>>()
.join(", ");
let keywords = (0..16)
.map(|i| format!("k{i}=a{i}"))
.collect::<Vec<_>>()
.join(", ");
let code = compile_exec(&format!(
"def f(obj, {params}):\n return obj.m({keywords})\n"
));
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let method_load_attr = f.instructions.iter().any(|unit| {
if let Instruction::LoadAttr { namei } = unit.op {
namei
.get(OpArg::new(u32::from(u8::from(unit.arg))))
.is_method()
} else {
false
}
});
let call_kw_16 = f.instructions.iter().any(|unit| match unit.op {
Instruction::CallKw { argc } => {
argc.get(OpArg::new(u32::from(u8::from(unit.arg)))) == 16
}
_ => false,
});
assert!(
method_load_attr && call_kw_16,
"CPython maybe_optimize_method_call emits LOAD_METHOD/CALL_KW under its own stack threshold, got ops={ops:?}"
);
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::CallFunctionEx)),
"method-call keyword path should not reuse codegen_call_helper_impl's lower kw threshold, got ops={ops:?}"
);
}
#[test]
fn large_plain_call_uses_direct_call_until_stack_guideline() {
let code = compile_exec(
"\
def f(g):
return g(a0, a1, a2, a3, a4, a5, a6, a7, a8,
a9, a10, a11, a12, a13, a14, a15, a16, a17)
",
);
let f = find_code(&code, "f").expect("missing function code");
let direct_call_18 = f.instructions.iter().any(|unit| match unit.op {
Instruction::Call { argc } => argc.get(OpArg::new(u32::from(u8::from(unit.arg)))) == 18,
_ => false,
});
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
direct_call_18,
"18 positional arguments should stay on CPython's direct CALL path, got ops={ops:?}"
);
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::CallFunctionEx)),
"18 positional arguments should not use CALL_FUNCTION_EX, got ops={ops:?}"
);
}
#[test]
fn too_large_plain_call_uses_cpython_tuple_ex_call_path() {
let args = (0..=STACK_USE_GUIDELINE)
.map(|i| format!("'v{i}'"))
.collect::<Vec<_>>()
.join(", ");
let code = compile_exec(&format!("def f(g):\n return g({args})\n"));
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.iter()
.any(|op| matches!(op, Instruction::CallFunctionEx)),
"CPython routes calls over _PY_STACK_USE_GUIDELINE through CALL_FUNCTION_EX, got ops={ops:?}"
);
assert!(
!ops.iter().any(|op| matches!(
op,
Instruction::BuildTuple { .. }
| Instruction::ListAppend { .. }
| Instruction::CallIntrinsic1 { .. }
)),
"CPython flowgraph.c folds the starunpack tuple for constant too-large calls, got ops={ops:?}"
);
assert!(
f.constants.iter().any(|constant| {
matches!(
constant,
ConstantData::Tuple { elements }
if elements.len() == usize::try_from(STACK_USE_GUIDELINE + 1).unwrap()
)
}),
"expected CPython folded tuple constant for too-large call args, got constants={:?}",
f.constants.iter().collect::<Vec<_>>()
);
}
#[test]
fn simple_attribute_call_keeps_method_load() {
let code = compile_exec(
"\
def f(obj, arg):
return obj.method(arg)
",
);
let f = find_code(&code, "f").expect("missing function code");
let load_attr = f
.instructions
.iter()
.find_map(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
Some(namei.get(OpArg::new(u32::from(u8::from(unit.arg)))))
}
_ => None,
})
.expect("missing LOAD_ATTR");
assert!(
load_attr.is_method(),
"simple method calls should stay optimized"
);
}
#[test]
fn starred_super_call_keeps_attr_line_nop() {
let code = compile_exec(
"\
def outer(log):
class DelegatingHTTPRequestHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args):
if log:
super(DelegatingHTTPRequestHandler,
self).log_message(format, *args)
",
);
let log_message = find_code(&code, "log_message").expect("missing log_message code");
let ops: Vec<_> = log_message
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadSuperAttr { .. },
Instruction::Nop,
Instruction::PushNull,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
]
)
}),
"starred super call should keep CPython's attr-line NOP after LOAD_SUPER_ATTR, got ops={ops:?}"
);
}
#[test]
fn builtin_any_genexpr_call_is_optimized() {
let code = compile_exec(
"\
def f(xs):
return any(x for x in xs)
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(has_common_constant(f, bytecode::CommonConstant::BuiltinAny));
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::PopJumpIfTrue { .. }))
);
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::NotTaken))
);
assert_eq!(
f.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::PushNull))
.count(),
1,
"fallback call path should remain for shadowed any()"
);
let genexpr_const_count = f
.constants
.iter()
.filter(|constant| {
matches!(constant, ConstantData::Code { code } if code.obj_name == "<genexpr>")
})
.count();
assert_eq!(
genexpr_const_count, 1,
"optimized and fallback any(genexpr) paths should share the same CPython-range code const"
);
assert_eq!(
f.linetable.as_ref(),
&[
0x80, 0x00, 0xdf, 0x0b, 0x0e, 0x8b, 0x33, 0x89, 0x6f, 0x99, 0x22, 0x8b, 0x6f, 0x8f,
0x33, 0x8c, 0x33, 0xd0, 0x04, 0x1d, 0x8a, 0x33, 0xd0, 0x04, 0x1d, 0x88, 0x33, 0x89,
0x6f, 0x99, 0x22, 0x8b, 0x6f, 0xd3, 0x0b, 0x1d, 0xd0, 0x04, 0x1d,
]
);
}
#[test]
fn builtin_any_async_genexpr_call_is_optimized_like_cpython() {
for source in [
"async def f(xs):\n return any(x async for x in xs)\n",
"async def f(xs):\n return any(await x for x in xs)\n",
] {
let code = compile_exec(source);
let f = find_code(&code, "f").expect("missing function code");
assert!(
has_common_constant(f, bytecode::CommonConstant::BuiltinAny),
"maybe_optimize_function_call() guards any(genexpr) whether or not the \
generator is a coroutine: {source}"
);
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::ForIter { .. })),
"the guarded path inlines the loop: {source}"
);
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::Call { .. })),
"the fallback still calls the name it loaded: {source}"
);
}
}
#[test]
fn builtin_any_genexpr_outermost_await_is_optimized_like_cpython() {
let code = compile_exec(
"\
async def f(get_xs):
return any(x for x in await get_xs())
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
has_common_constant(f, bytecode::CommonConstant::BuiltinAny),
"CPython checks the generator expression symtable entry, so await in the outermost iterator does not make the genexpr coroutine"
);
}
#[test]
fn builtin_tuple_genexpr_call_is_optimized_but_list_set_are_not() {
let code = compile_exec(
"\
def tuple_f(xs):
return tuple(x for x in xs)
def list_f(xs):
return list(x for x in xs)
def set_f(xs):
return set(x for x in xs)
",
);
let tuple_f = find_code(&code, "tuple_f").expect("missing tuple_f code");
assert!(has_common_constant(
tuple_f,
bytecode::CommonConstant::BuiltinTuple
));
assert!(has_intrinsic_1(tuple_f, IntrinsicFunction1::ListToTuple));
let tuple_list_append = tuple_f
.instructions
.iter()
.find_map(|unit| match unit.op {
Instruction::ListAppend { .. } => Some(u32::from(u8::from(unit.arg))),
_ => None,
})
.expect("tuple(genexpr) fast path should emit LIST_APPEND");
assert_eq!(tuple_list_append, 2);
assert_eq!(
tuple_f.linetable.as_ref(),
&[
0x80, 0x00, 0xdf, 0x0b, 0x10, 0x8c, 0x35, 0x91, 0x0f, 0x99, 0x42, 0x93, 0x0f, 0x8f,
0x35, 0xd0, 0x04, 0x1f, 0x88, 0x35, 0x91, 0x0f, 0x99, 0x42, 0x93, 0x0f, 0xd3, 0x0b,
0x1f, 0xd0, 0x04, 0x1f,
]
);
let list_f = find_code(&code, "list_f").expect("missing list_f code");
assert!(
list_f
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::Call { .. })),
"list(genexpr) should stay on the normal call path"
);
assert!(
!has_common_constant(list_f, bytecode::CommonConstant::BuiltinList),
"CPython 3.14.5 does not optimize list(genexpr)"
);
let set_f = find_code(&code, "set_f").expect("missing set_f code");
assert!(
set_f
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::Call { .. })),
"set(genexpr) should stay on the normal call path"
);
assert!(
!has_common_constant(set_f, bytecode::CommonConstant::BuiltinSet),
"CPython 3.14.5 does not optimize set(genexpr)"
);
}
#[test]
fn builtin_tuple_genexpr_try_assignment_uses_shared_tail() {
let code = compile_exec(
"\
def f(xs):
global y
try:
y = tuple(int(i) for i in xs.split('.'))
except ValueError:
y = ()
return y
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let intrinsic = ops
.iter()
.position(|op| matches!(op, Instruction::CallIntrinsic1 { .. }))
.expect("tuple(genexpr) fast path should emit LIST_TO_TUPLE");
let first_fallback = ops[intrinsic + 1..]
.iter()
.position(|op| matches!(op, Instruction::PushNull))
.map(|offset| intrinsic + 1 + offset)
.expect("shadowed tuple fallback call should remain after fast path");
let first_store = ops[intrinsic + 1..]
.iter()
.position(|op| matches!(op, Instruction::StoreGlobal { .. }))
.map(|offset| intrinsic + 1 + offset)
.expect("tuple(genexpr) result should be stored after fast or fallback call");
assert!(
matches!(ops[intrinsic + 1], Instruction::JumpForward { .. })
&& first_fallback < first_store,
"tuple(genexpr) fast path should jump over fallback to CPython-style shared store tail, got ops={ops:?}"
);
}
#[test]
fn builtin_tuple_genexpr_unprotected_assignment_return_duplicates_tail() {
let code = compile_exec(
"\
def f(arg):
if isinstance(arg, (list, tuple)):
arg = tuple(a for a in arg)
elif not p(arg):
raise TypeError(f'bad {arg}')
return arg
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let intrinsic = ops
.iter()
.position(|op| matches!(op, Instruction::CallIntrinsic1 { .. }))
.expect("tuple(genexpr) fast path should emit LIST_TO_TUPLE");
assert!(
matches!(ops[intrinsic + 1], Instruction::StoreFast { .. })
&& matches!(
ops[intrinsic + 2],
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. }
)
&& matches!(ops[intrinsic + 3], Instruction::ReturnValue),
"unprotected tuple(genexpr) assignment before return should inline CPython's assignment-return tail, got ops={ops:?}"
);
}
#[test]
fn unprotected_builtin_any_prefix_before_returning_try_keeps_borrow() {
let code = compile_exec(
"\
def f(template):
if any(part.expression.strip() == '' for part in template.interpolations):
return ctor(template)
try:
parsed = tuple(
ast.parse(f'({part.expression})', mode='eval').body
for part in template.interpolations
)
except SyntaxError:
return ctor(template)
return lit(template, parsed)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let first_try_nop = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::Nop))
.expect("missing try entry NOP");
let mut saw_interpolations = false;
for window in instructions[..first_try_nop].windows(2) {
let [receiver, attr] = window else {
continue;
};
let Instruction::LoadAttr { namei } = attr.op else {
continue;
};
let load_attr = namei.get(OpArg::new(u32::from(u8::from(attr.arg))));
if f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() != "interpolations"
{
continue;
}
saw_interpolations = true;
assert!(
matches!(receiver.op, Instruction::LoadFastBorrow { .. }),
"unprotected builtin any(genexpr) prefix before a later returning try should keep CPython-style borrowed receiver, got instructions={instructions:?}"
);
}
assert!(
saw_interpolations,
"missing interpolations attr load in builtin any prefix, got instructions={instructions:?}"
);
}
#[test]
fn module_store_uses_store_global_when_nested_scope_declares_global() {
let code = compile_exec(
"\
_address_fmt_re = None
class C:
def f(self):
global _address_fmt_re
if _address_fmt_re is None:
_address_fmt_re = 1
",
);
assert!(code.instructions.iter().any(|unit| match unit.op {
Instruction::StoreGlobal { namei } => {
let idx = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
code.names[usize::try_from(idx).unwrap()].as_str() == "_address_fmt_re"
}
_ => false,
}));
}
#[test]
fn conditional_return_epilogue_is_duplicated() {
let code = compile_exec(
"\
def f(base, cls, state):
if base is object:
obj = object.__new__(cls)
else:
obj = base.__new__(cls, state)
return obj
",
);
let f = find_code(&code, "f").expect("missing function code");
let return_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::ReturnValue))
.count();
assert_eq!(return_count, 2);
}
#[test]
fn loop_store_subscr_threads_direct_backedge() {
let code = compile_exec(
"\
def f(kwonlyargs, kw_only_defaults, arg2value):
missing = 0
for kwarg in kwonlyargs:
if kwarg not in arg2value:
if kw_only_defaults and kwarg in kw_only_defaults:
arg2value[kwarg] = kw_only_defaults[kwarg]
else:
missing += 1
return missing
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let store_subscr = ops
.iter()
.position(|op| matches!(op, Instruction::StoreSubscr))
.expect("missing STORE_SUBSCR");
let next_op = ops
.get(store_subscr + 1)
.expect("missing jump after STORE_SUBSCR");
let window_start = store_subscr.saturating_sub(3);
let window_end = (store_subscr + 5).min(ops.len());
let window = &ops[window_start..window_end];
assert!(
matches!(next_op, Instruction::JumpBackward { .. }),
"expected direct loop backedge after STORE_SUBSCR, got {next_op:?}; ops={window:?}"
);
}
#[test]
fn protected_store_subscr_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(cache, lock, format):
with lock:
format_regex = cache.get(format)
if not format_regex:
try:
format_regex = cache.compile(format)
except KeyError as err:
bad_directive = err.args[0]
del err
raise ValueError(bad_directive) from None
cache[format] = format_regex
return format_regex.match('x')
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadFastLoadFast { .. },
Instruction::LoadFast { .. },
Instruction::StoreSubscr,
Instruction::LoadConst { .. },
]
)
}),
"expected CPython-style strong loads before protected STORE_SUBSCR tail, got ops={ops:?}"
);
let code = compile_exec(
"\
cache = {}
def g(lock, format):
with lock:
format_regex = cache.get(format)
if not format_regex:
try:
format_regex = compile(format)
except KeyError as err:
bad_directive = err.args[0]
del err
raise ValueError(bad_directive) from None
cache[format] = format_regex
return format_regex.match('x')
",
);
let g = find_code(&code, "g").expect("missing function code");
let ops: Vec<_> = g
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadFast { .. },
Instruction::LoadGlobal { .. },
Instruction::LoadFast { .. },
Instruction::StoreSubscr,
]
)
}),
"expected CPython-style strong value/key loads around global STORE_SUBSCR tail, got ops={ops:?}"
);
}
#[test]
fn try_except_inner_for_cleanup_allows_try_end_borrow() {
let code = compile_exec(
"\
def f(self, futures, already_completed, future, short_timeout):
for timeout in (0, short_timeout):
with self.subTest(timeout):
completed_futures = set()
try:
for item in futures.as_completed(already_completed | {future}, timeout):
completed_futures.add(item)
except futures.TimeoutError:
pass
self.assertEqual(completed_futures, already_completed)
",
);
let f = find_code(&code, "f").expect("missing function code");
let borrow_pair_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::LoadFastBorrowLoadFastBorrow { .. }))
.count();
assert!(
borrow_pair_count >= 2,
"expected CPython-style borrowed pair loads before and after inner for cleanup, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn augassign_two_part_slice_uses_slice_opcodes() {
let code = compile_exec(
"\
def aug(x, a, b, y):
x[a:b] += y
",
);
let aug = find_code(&code, "aug").expect("missing aug code");
let ops: Vec<_> = aug
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert_eq!(
ops.iter()
.filter(|op| matches!(op, Instruction::BinarySlice))
.count(),
1,
"expected one BINARY_SLICE in augassign slice path, got ops={ops:?}"
);
assert_eq!(
ops.iter()
.filter(|op| matches!(op, Instruction::StoreSlice))
.count(),
1,
"expected one STORE_SLICE in augassign slice path, got ops={ops:?}"
);
assert!(
!ops.iter().any(|op| {
matches!(
op,
Instruction::BuildSlice { .. } | Instruction::StoreSubscr
)
}),
"two-part augassign slice should avoid BUILD_SLICE/STORE_SUBSCR, got ops={ops:?}"
);
assert!(
ops.windows(10).any(|window| {
matches!(
window,
[
Instruction::Copy { .. },
Instruction::Copy { .. },
Instruction::Copy { .. },
Instruction::BinarySlice,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::BinaryOp { .. },
Instruction::Swap { .. },
Instruction::Swap { .. },
Instruction::Swap { .. },
Instruction::StoreSlice,
]
)
}),
"expected CPython-style augassign slice window, got ops={ops:?}"
);
}
#[test]
fn augassign_constant_slice_copy_uses_subscript_location_like_cpython() {
let code = compile_exec(
"\
def aug_const(x, y):
x[1:2] += y
",
);
let aug_const = find_code(&code, "aug_const").expect("missing aug_const code");
// CPython 3.14 codegen_augassign() visits a constant slice, then emits
// COPY/COPY/BINARY_OP NB_SUBSCR at LOC(target), not at LOC(slice).
assert_eq!(
aug_const.linetable.as_ref(),
&[
0x80, 0x00, 0xd8, 0x04, 0x05, 0x80, 0x63, 0x87, 0x46, 0x88, 0x61, 0x85, 0x4b, 0x85,
0x46,
]
);
}
#[test]
fn augassign_attribute_copy_uses_target_location_like_cpython() {
let code = compile_exec(
"\
def f(obj, value):
obj.attr += value
",
);
let f = find_code(&code, "f").expect("missing f code");
let copy_position = f
.instructions
.iter()
.zip(&f.locations)
.find_map(|(unit, (location, end_location))| {
let Instruction::Copy { i } = unit.op else {
return None;
};
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
(i.get(arg) == 1).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.expect("missing augmented attribute COPY");
assert_eq!(
copy_position,
(2, 5, 2, 13),
"CPython codegen_augassign() emits COPY 1 at LOC(target) before updating to attr location"
);
}
#[test]
fn loop_return_reorders_backedge_before_exit_cleanup() {
let code = compile_exec(
"\
def f(obj):
for base in obj.__mro__:
if base is not object:
doc = base.__doc__
if doc is not None:
return doc
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let has_cpython_shape = ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::PopJumpIfNotNone { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::Swap { .. },
Instruction::PopTop,
Instruction::ReturnValue,
]
)
});
assert!(
has_cpython_shape,
"expected loop return null-check to keep the backedge adjacent to the return cleanup, got ops={ops:?}"
);
let end_for_idx = ops
.iter()
.position(|op| matches!(op, Instruction::EndFor))
.expect("missing END_FOR");
let return_before_end = ops[..end_for_idx]
.iter()
.rposition(|op| matches!(op, Instruction::ReturnValue))
.expect("missing loop-body RETURN_VALUE");
assert!(
matches!(ops.get(return_before_end - 1), Some(Instruction::PopTop)),
"expected POP_TOP before loop-body RETURN_VALUE, got {:?}; ops={ops:?}",
ops.get(return_before_end.saturating_sub(1))
);
}
#[test]
fn nested_try_finally_cleanup_reorder_does_not_invert_forward_jumps() {
compile_exec(include_str!("../../../Lib/poplib.py"));
}
#[test]
fn conditional_body_is_preserved_before_final_return() {
let code = compile_exec(
"\
def f(x, y):
if x == y:
print('then', flush=True)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let cond_idx = ops
.iter()
.position(|op| matches!(op, Instruction::PopJumpIfFalse { .. }))
.expect("missing POP_JUMP_IF_FALSE");
let first_return_idx = ops
.iter()
.position(|op| matches!(op, Instruction::ReturnValue))
.expect("missing RETURN_VALUE");
assert!(
ops[cond_idx..first_return_idx]
.iter()
.any(|op| matches!(op, Instruction::CallKw { .. })),
"expected conditional body call before final return, got ops={ops:?}"
);
}
#[test]
fn nested_conditional_body_is_preserved_before_final_return() {
let code = compile_exec(
"\
def outer():
def side():
print('side', flush=True)
def cb():
flag = True
if flag:
side()
return cb
",
);
let cb = find_code(&code, "cb").expect("missing nested cb code");
let ops: Vec<_> = cb
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let cond_idx = ops
.iter()
.position(|op| matches!(op, Instruction::PopJumpIfFalse { .. }))
.expect("missing POP_JUMP_IF_FALSE");
let first_return_idx = ops
.iter()
.position(|op| matches!(op, Instruction::ReturnValue))
.expect("missing RETURN_VALUE");
assert!(
ops[cond_idx..first_return_idx]
.iter()
.any(|op| matches!(op, Instruction::Call { .. })),
"expected nested conditional body call before final return, got ops={ops:?}"
);
}
#[test]
fn try_line_nop_is_preserved_before_setup_finally() {
let code = compile_exec(
"\
def f(msg):
try:
fw = _wm.formatwarning
except AttributeError:
pass
else:
if fw is not _formatwarning_orig:
return fw(msg.message, msg.category, msg.filename, msg.lineno, msg.line)
return _wm._formatwarnmsg_impl(msg)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
matches!(
ops.as_slice(),
[Instruction::Resume { .. }, Instruction::Nop, ..]
),
"expected CPython try-line NOP before setup/fetch, got ops={ops:?}"
);
}
#[test]
fn nested_try_line_nops_after_for_cleanup_are_preserved() {
let code = compile_exec(
"\
def f(xs, env):
for x in xs:
pass
try:
try:
if env is not None:
env_list = []
else:
env_list = None
finally:
pass
finally:
pass
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::Nop,
Instruction::Nop,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::PopJumpIfNone { .. },
]
)
}),
"expected CPython-style outer and inner try-line NOPs after for cleanup, got ops={ops:?}"
);
}
#[test]
fn try_finally_assert_keeps_finalbody_entry_nop() {
let code = compile_exec(
"\
def f(x):
try:
assert x
finally:
g()
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::RaiseVarargs { .. },
Instruction::Nop,
Instruction::LoadGlobal { .. },
Instruction::Call { .. },
]
)
}),
"assert in try/finally should preserve CPython finalbody-entry NOP after the raise edge, got ops={ops:?}"
);
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::LoadCommonConstant { .. },
Instruction::RaiseVarargs { .. },
Instruction::Nop,
Instruction::LoadGlobal { .. },
]
)
}),
"assert true edge should land on a distinct finalbody-entry NOP, got ops={ops:?}"
);
}
#[test]
fn try_finally_if_break_false_edge_keeps_finalbody_entry_nop() {
let code = compile_exec(
"\
def f(self, pid):
while True:
try:
if pid == self.pid:
self.h()
break
finally:
self.r()
self.g()
return self.x
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::ReturnValue,
Instruction::Nop,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::Call { .. },
Instruction::PopTop,
]
)
}),
"expected CPython-style if-line NOP before fallthrough finally body, got ops={ops:?}"
);
}
#[test]
fn try_percent_format_preprocess_removes_redundant_try_nop() {
let code = compile_exec(
"\
def f(self, signal):
if self.returncode and self.returncode < 0:
try:
return \"Command '%s' died with %r.\" % (
self.cmd, signal.Signals(-self.returncode))
except ValueError:
return \"Command '%s' died with unknown signal %d.\" % (
self.cmd, -self.returncode)
return \"Command '%s' returned non-zero exit status %d.\" % (
self.cmd, self.returncode)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::NotTaken,
Instruction::LoadConst { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
]
)
}),
"expected preprocessed percent-format body immediately after condition, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::NotTaken,
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
]
)
}),
"percent-format preprocessing should let CFG remove the try-line NOP, got ops={ops:?}"
);
}
#[test]
fn nested_try_except_in_finally_exception_path_shares_continuation() {
let code = compile_exec(
"\
def f(self, exc_type, KeyboardInterrupt, TimeoutExpired):
try:
if self.stdin:
self.stdin.close()
finally:
if exc_type == KeyboardInterrupt:
if self._sigint_wait_secs > 0:
try:
self._wait(timeout=self._sigint_wait_secs)
except TimeoutExpired:
pass
self._sigint_wait_secs = 0
else:
self.wait()
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let store_reraise_tails = ops
.windows(2)
.filter(|window| {
matches!(
window,
[Instruction::StoreAttr { .. }, Instruction::Reraise { .. },]
)
})
.count();
assert_eq!(
store_reraise_tails, 1,
"nested try/except inside an exceptional finally body should share the remaining finalbody tail before RERAISE, got ops={ops:?}"
);
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadSmallInt { .. },
Instruction::LoadFastBorrow { .. },
Instruction::StoreAttr { .. },
Instruction::LoadConst { .. },
Instruction::ReturnValue,
]
)
}),
"normal finally body should keep CPython-style borrowed load before STORE_ATTR, got ops={ops:?}"
);
}
#[test]
fn try_else_return_keeps_nop_before_final_call_return() {
let code = compile_exec(
"\
def f(msg):
try:
fw = _wm.formatwarning
except AttributeError:
pass
else:
if fw is not _formatwarning_orig:
return fw(msg.message, msg.category, msg.filename, msg.lineno, msg.line)
return _wm._formatwarnmsg_impl(msg)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::ReturnValue,
Instruction::Nop,
Instruction::LoadGlobal { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::Call { .. },
Instruction::ReturnValue,
]
)
}),
"expected CPython-style NOP between conditional return and final call return, got ops={ops:?}"
);
}
#[test]
fn try_else_conditional_scope_exit_keeps_pop_block_nop() {
let code = compile_exec(
"\
def f(values, check):
found = ''
for value in values:
try:
if check(value):
raise UnicodeError
except UnicodeError:
pass
else:
found = value
break
return found
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::RaiseVarargs { .. },
Instruction::Nop,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
Instruction::StoreFast { .. },
Instruction::PopTop,
]
)
}),
"try-else after conditional scope exit should keep CPython's POP_BLOCK NOP anchor, got ops={ops:?}"
);
}
#[test]
fn try_else_loop_fallthrough_keeps_end_jump_nop_before_finally() {
let code = compile_exec(
"\
def f(locale, category, locales):
try:
orig_locale = locale.setlocale(category)
except AttributeError:
raise
except Exception:
locale = orig_locale = None
if '' not in locales:
raise SkipTest('no locales')
else:
for loc in locales:
try:
locale.setlocale(category, loc)
break
except locale.Error:
pass
else:
if '' not in locales:
raise SkipTest(locales)
try:
yield
finally:
if locale and orig_locale:
locale.setlocale(category, orig_locale)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::RaiseVarargs { .. },
Instruction::Nop,
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::YieldValue { .. },
]
)
}),
"try-else loop fallthrough should keep CPython's end-label NOP before following try/finally, got ops={ops:?}"
);
}
#[test]
fn conditional_compare_uses_bool_compare_oparg() {
let code = compile_exec(
"\
def f(x, y):
if x == y:
return 1
return 0
",
);
let f = find_code(&code, "f").expect("missing function code");
let compare = f
.instructions
.iter()
.find(|unit| matches!(unit.op, Instruction::CompareOp { .. }))
.expect("missing COMPARE_OP");
assert_eq!(u8::from(compare.arg), 88);
}
#[test]
fn multiline_is_none_conditional_keeps_comparator_nop() {
let code = compile_exec(
"\
def f(x):
if x.find(
'a') is not None:
return 1
return 0
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::Call { .. },
Instruction::Nop,
Instruction::PopJumpIfNone { .. },
]
)
}),
"expected CPython-style comparator NOP before folded POP_JUMP_IF_NONE, got ops={ops:?}"
);
}
#[test]
fn chained_conditional_compares_use_bool_compare_oparg() {
let code = compile_exec(
"\
def f(a, b, c):
if a < b < c:
return 1
return 0
",
);
let f = find_code(&code, "f").expect("missing function code");
let compare_args: Vec<_> = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::CompareOp { .. }))
.map(|unit| u8::from(unit.arg))
.collect();
assert_eq!(compare_args, vec![18, 18]);
}
#[test]
fn shared_final_return_is_cloned_for_jump_target() {
let code = compile_exec(
"\
def f(node):
if not isinstance(
node, (AsyncFunctionDef, FunctionDef, ClassDef, Module)
) or len(node.body) < 1:
return None
node = node.body[0]
if not isinstance(node, Expr):
return None
node = node.value
if isinstance(node, Constant) and isinstance(node.value, str):
return node
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let return_count = ops
.iter()
.filter(|op| matches!(op, Instruction::ReturnValue))
.count();
assert_eq!(
return_count, 5,
"expected cloned return sites for each shared return edge, got ops={ops:?}"
);
}
#[test]
fn for_break_uses_poptop_cleanup() {
let code = compile_exec(
"\
def f(parts):
for value in parts:
if value:
break
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let pop_iter_count = ops
.iter()
.filter(|op| matches!(op, Instruction::PopIter))
.count();
assert_eq!(
pop_iter_count, 1,
"expected only the loop-exhaustion POP_ITER, got ops={ops:?}"
);
let break_cleanup_idx = ops
.windows(3)
.position(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::LoadConst { .. },
Instruction::ReturnValue
]
)
})
.expect("missing POP_TOP/LOAD_CONST/RETURN_VALUE break cleanup");
let end_for_idx = ops
.iter()
.position(|op| matches!(op, Instruction::EndFor))
.expect("missing END_FOR");
assert!(
break_cleanup_idx < end_for_idx,
"expected break cleanup before END_FOR, got ops={ops:?}"
);
}
#[test]
fn for_exit_before_elif_does_not_leave_line_anchor_nop() {
let code = compile_exec(
"\
from sys import maxsize
if maxsize == 2147483647:
for s in ('2147483648', '0o40000000000', '0x100000000', '0b10000000000000000000000000000000'):
try:
x = eval(s)
except OverflowError:
fail('OverflowError on huge integer literal %r' % s)
elif maxsize == 9223372036854775807:
pass
",
);
let ops: Vec<_> = code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::LoadConst { .. },
Instruction::ReturnValue,
]
)
}),
"expected for-exit epilogue without extra NOP, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::Nop,
Instruction::LoadConst { .. },
]
)
}),
"unexpected line-anchor NOP before for-exit epilogue, got ops={ops:?}"
);
}
#[test]
fn for_tuple_target_does_not_leave_loop_header_nop() {
let code = compile_exec(
"\
def f(pairs):
for left, right in pairs:
pass
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(2).any(|window| {
matches!(
window,
[
Instruction::ForIter { .. },
Instruction::UnpackSequence { .. }
]
)
}),
"expected FOR_ITER to flow directly into UNPACK_SEQUENCE, got ops={ops:?}"
);
assert!(
!ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::ForIter { .. },
Instruction::Nop,
Instruction::UnpackSequence { .. },
]
)
}),
"unexpected loop-header NOP before tuple unpack, got ops={ops:?}"
);
}
#[test]
fn tstring_build_template_matches_cpython_stack_order() {
let code = compile_exec("t = t\"{0}\"");
let units: Vec<_> = code
.instructions
.iter()
.copied()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
assert!(
units.windows(6).any(|window| {
matches!(
window,
[
a,
b,
c,
d,
e,
f,
]
if matches!(a.op, Instruction::LoadConst { .. })
&& matches!(b.op, Instruction::LoadSmallInt { .. })
&& matches!(c.op, Instruction::LoadConst { .. })
&& matches!(d.op, Instruction::BuildInterpolation { .. })
&& u8::from(d.arg) == 2
&& matches!(e.op, Instruction::BuildTuple { .. })
&& u8::from(e.arg) == 1
&& matches!(f.op, Instruction::BuildTemplate)
)
}),
"expected CPython-style t-string lowering, got units={units:?}"
);
assert!(
!units
.iter()
.any(|unit| matches!(unit.op, Instruction::Swap { .. })),
"unexpected SWAP in t-string lowering, got units={units:?}"
);
}
#[test]
fn tstring_debug_specifier_uses_debug_literal_and_repr_default() {
let code = compile_exec(
"\
value = 42
t = t\"Value: {value=}\"
",
);
let string_consts = code
.instructions
.iter()
.filter_map(|unit| match unit.op {
Instruction::LoadConst { consti } => {
Some(&code.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))])
}
_ => None,
})
.collect::<Vec<_>>();
assert!(
string_consts.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if matches!(
&elements[..],
[
ConstantData::Str { value: first },
ConstantData::Str { value: second },
] if first.to_string() == "Value: value=" && second.is_empty()
)
)),
"expected debug literal prefix in t-string constants, got {string_consts:?}"
);
assert!(
code.instructions.iter().any(|unit| matches!(
unit.op,
Instruction::BuildInterpolation { .. }
) && u8::from(unit.arg) == 10),
"expected default repr conversion for debug t-string"
);
}
#[test]
fn tstring_ops_restore_template_and_interpolation_locations_like_cpython() {
let code = compile_exec(
"\
def f(x):
return t\"{x}\"
",
);
let f = find_code(&code, "f").expect("missing f code");
let mut build_interpolation = None;
let mut build_tuple = None;
let mut build_template = None;
for (unit, (location, end_location)) in f.instructions.iter().zip(&f.locations) {
let range = (
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
);
match unit.op {
Instruction::BuildInterpolation { .. } => build_interpolation = Some(range),
Instruction::BuildTuple { .. } => build_tuple = Some(range),
Instruction::BuildTemplate => build_template = Some(range),
_ => {}
}
}
assert_eq!(
build_interpolation,
Some((2, 14, 2, 17)),
"CPython codegen_interpolation() restores LOC(Interpolation) after visiting the value; this direct codegen path uses the parser's Interpolation range"
);
assert_eq!(
build_tuple,
Some((2, 12, 2, 18)),
"CPython codegen_template_str() emits the interpolations tuple at LOC(TemplateStr); this direct codegen path uses the parser's TemplateStr range"
);
assert_eq!(
build_template,
Some((2, 12, 2, 18)),
"CPython codegen_template_str() emits BUILD_TEMPLATE at LOC(TemplateStr); this direct codegen path uses the parser's TemplateStr range"
);
}
#[test]
fn regular_call_push_null_uses_callee_location_like_cpython() {
let code = compile_exec(
"\
def f(g, x):
return (
g
)(x)
",
);
let f = find_code(&code, "f").expect("missing f code");
let push_null = f
.instructions
.iter()
.zip(&f.locations)
.find_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::PushNull).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.expect("missing PUSH_NULL");
assert_eq!(
push_null,
(3, 9, 3, 10),
"CPython codegen_call() resets loc to LOC(func) before emitting PUSH_NULL; this direct codegen path uses the parser's callee range"
);
}
#[test]
fn tstring_literal_preserves_surrogate_wtf8() {
let code = compile_exec("t = t\"\\ud800\"");
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Str { value } if value.clone().into_bytes() == [0xED, 0xA0, 0x80]
)));
}
#[test]
fn break_in_finally_after_return_keeps_load_fast_check_for_loop_locals() {
let code = compile_exec(
"\
def g2(x):
for count in [0, 1]:
for count2 in [10, 20]:
try:
return count + count2
finally:
if x:
break
return 'end', count, count2
",
);
let g2 = find_code(&code, "g2").expect("missing g2 code");
let ops: Vec<_> = g2
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadConst { .. },
Instruction::LoadFastCheck { .. },
Instruction::LoadFastCheck { .. },
Instruction::BuildTuple { .. },
]
)
}),
"expected LOAD_FAST_CHECK pair for after-return loop locals, got ops={ops:?}"
);
}
#[test]
fn high_index_parameter_stays_initialized_in_fast_scan() {
let params = (0..65)
.map(|idx| format!("p{idx}"))
.collect::<Vec<_>>()
.join(", ");
let code = compile_exec(&format!(
"\
def f({params}):
return p64
"
));
let f = find_code(&code, "f").expect("missing f code");
assert!(
f.instructions.iter().any(|unit| matches!(
unit.op,
Instruction::LoadFastCheck { var_num }
if f.varnames
[usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))))]
== "p64"
)),
"CPython 3.14 fast_scan_many_locals() checks high-index parameters per block; expected p64 to use LOAD_FAST_CHECK, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn deleted_high_index_parameter_uses_load_fast_check() {
let params = (0..65)
.map(|idx| format!("p{idx}"))
.collect::<Vec<_>>()
.join(", ");
let code = compile_exec(&format!(
"\
def f({params}):
del p64
return p64
"
));
let f = find_code(&code, "f").expect("missing f code");
assert!(
f.instructions.iter().any(|unit| matches!(
unit.op,
Instruction::LoadFastCheck { var_num }
if f.varnames
[usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))))]
== "p64"
)),
"expected deleted high-index parameter p64 to use LOAD_FAST_CHECK, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn assert_without_message_raises_class_directly() {
let code = compile_exec(
"\
def f(x):
assert x
",
);
let f = find_code(&code, "f").expect("missing function code");
let call_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::Call { .. }))
.count();
let push_null_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::PushNull))
.count();
assert_eq!(call_count, 0);
assert_eq!(push_null_count, 0);
}
#[test]
fn assert_with_message_uses_common_constant_direct_call() {
let code = compile_exec(
"\
def f(x, y):
assert x, y
",
);
let f = find_code(&code, "f").expect("missing f code");
let load_assertion = f
.instructions
.iter()
.position(|unit| {
matches!(unit.op, Instruction::LoadCommonConstant { .. })
&& matches!(
unit.op,
Instruction::LoadCommonConstant { idx }
if idx.get(OpArg::new(u32::from(u8::from(unit.arg))))
== bytecode::CommonConstant::AssertionError
)
})
.expect("missing LOAD_COMMON_CONSTANT AssertionError");
assert!(
!matches!(
f.instructions.get(load_assertion + 1).map(|unit| unit.op),
Some(Instruction::PushNull)
),
"assert message path should not use PUSH_NULL, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
assert!(
matches!(
f.instructions.get(load_assertion + 2).map(|unit| unit.op),
Some(Instruction::Call { .. })
),
"expected direct CALL after loading assert message, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
let call_arg = f.instructions[load_assertion + 2].arg;
assert_eq!(u8::from(call_arg), 0);
}
#[test]
fn conditional_assert_message_target_uses_strong_load_fast() {
let code = compile_exec(
"\
def f(fname):
if fname == 'a':
return 1
assert False, 'Unknown attrname %s' % fname
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let assertion_error = ops
.iter()
.position(|op| matches!(op, Instruction::LoadCommonConstant { .. }))
.expect("missing LOAD_COMMON_CONSTANT AssertionError");
let window = &ops[assertion_error..(assertion_error + 5).min(ops.len())];
assert!(
matches!(
window,
[
Instruction::LoadCommonConstant { .. },
Instruction::LoadConst { .. },
Instruction::LoadFast { .. },
Instruction::BinaryOp { .. },
Instruction::Call { .. },
..
]
),
"expected CPython-style strong LOAD_FAST in targeted assert message block, got {window:?}"
);
}
#[test]
fn chained_compare_assert_message_keeps_borrowed_load_fast() {
let code = compile_exec(
"\
def f(month):
assert 1 <= month <= 12, f'month must be in 1..12, not {month}'
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let assertion_error = ops
.iter()
.position(|op| matches!(op, Instruction::LoadCommonConstant { .. }))
.expect("missing LOAD_COMMON_CONSTANT AssertionError");
let raise = ops[assertion_error..]
.iter()
.position(|op| matches!(op, Instruction::RaiseVarargs { .. }))
.map(|idx| assertion_error + idx)
.expect("missing assert raise");
let message_path = &ops[assertion_error..raise];
assert!(
message_path
.iter()
.any(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"CPython keeps assert message loads borrowed for same-line chained compare failure blocks, got {message_path:?}"
);
assert!(
!message_path
.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"same-line chained compare assert message should not be deoptimized to LOAD_FAST, got {message_path:?}"
);
}
#[test]
fn assert_message_after_condition_in_same_block_keeps_borrowed_loads() {
let code = compile_exec(
"\
def f(expected_ns, namespace):
try:
assert expected_ns == namespace, ('expected %s, got %s' % (expected_ns, namespace))
except AssertionError as e:
raise RuntimeError(e)
setattr(namespace, 'spam', expected_ns)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let assertion_error = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::LoadCommonConstant { .. }))
.expect("missing LOAD_COMMON_CONSTANT AssertionError");
let raise = ops[assertion_error..]
.iter()
.position(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. }))
.map(|idx| assertion_error + idx)
.expect("missing assert raise");
let message_path = &ops[assertion_error..raise];
let load_fast_name = |unit: &&bytecode::CodeUnit| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
let borrow_name = |unit: &&bytecode::CodeUnit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
assert!(
message_path
.iter()
.filter_map(load_fast_name)
.all(|name| name != "expected_ns" && name != "namespace"),
"assert message after same-block condition should keep borrowed loads, got {message_path:?}"
);
for name in ["expected_ns", "namespace"] {
assert!(
message_path
.iter()
.filter_map(borrow_name)
.any(|var| var == name),
"expected borrowed {name} load in assert message path, got {message_path:?}"
);
}
let setattr = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::LoadGlobal { .. }))
.expect("missing final setattr load");
let tail = &ops[setattr..];
for name in ["expected_ns", "namespace"] {
assert!(
tail.iter()
.filter_map(load_fast_name)
.any(|var| var == name),
"expected strong {name} load in post-try tail, got {tail:?}"
);
}
}
#[test]
fn bare_function_annotations_check_attribute_and_subscript_expressions() {
let code = compile_exec(
"\
def f(one: int):
int.new_attr: int
[list][0].new_attr: [int, str]
my_lst = [1]
my_lst[one]: int
return my_lst
",
);
let f = find_code(&code, "f").expect("missing f code");
let loads_global = |name: &str| {
f.instructions.iter().any(|unit| match unit.op {
Instruction::LoadGlobal { namei } => {
let namei = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) >> 1;
f.names[usize::try_from(namei).unwrap()].as_str() == name
}
_ => false,
})
};
assert!(
loads_global("int"),
"bare attribute annotations should evaluate int, got ops={:?}",
f.instructions
);
assert!(
loads_global("list"),
"bare subscript annotations should evaluate list, got ops={:?}",
f.instructions
);
}
#[test]
fn function_local_annassign_annotation_does_not_capture_outer_local() {
let code = compile_exec(
"\
def f():
from collections import namedtuple
class MyHandler:
def __init__(self, resource):
self.resource: namedtuple = resource
return namedtuple
",
);
let f = find_code(&code, "f").expect("missing f code");
let class_code = find_code(&code, "MyHandler").expect("missing MyHandler code");
let init = find_code(&code, "__init__").expect("missing __init__ code");
assert!(
!f.cellvars.iter().any(|name| name == "namedtuple"),
"function-local AnnAssign annotation must not make namedtuple a cell, got cellvars={:?}",
f.cellvars
);
assert!(
!class_code.freevars.iter().any(|name| name == "namedtuple"),
"class body must not close over function-local annotation-only name, got freevars={:?}",
class_code.freevars
);
assert!(
!init.freevars.iter().any(|name| name == "namedtuple"),
"method body must not close over function-local annotation-only name, got freevars={:?}",
init.freevars
);
}
#[test]
fn finally_exception_path_inlines_except_pass_reraise_tail() {
let source = "\
def f(self, file, backupfilename):
try:
if file:
file.close()
finally:
backupfilename = self._backupfilename
self._backupfilename = None
if backupfilename and not self._backup:
try:
os.unlink(backupfilename)
except OSError:
pass
self._isstdin = False
";
let code = compile_exec(source);
let f = find_code(&code, "f").expect("missing f code");
assert!(
f.instructions.windows(9).any(|window| {
matches!(
[
window[0].op,
window[1].op,
window[2].op,
window[3].op,
window[8].op,
],
[
Instruction::PopExcept,
Instruction::LoadConst { .. },
Instruction::LoadFast { .. },
Instruction::StoreAttr { .. },
Instruction::Reraise { .. },
]
) && match window[8].op {
Instruction::Reraise { depth } => {
depth.get(OpArg::new(u32::from(u8::from(window[8].arg)))) == 0
}
_ => false,
}
}),
"except-pass normal exit in a finally exception path should inline the CPython reraise tail, got instructions={:?}",
f.instructions
);
}
#[test]
fn nested_finally_exception_path_prunes_dead_normal_cleanup() {
let code = compile_exec(
"\
def f():
try:
raise ValueError
finally:
try:
raise KeyError
finally:
1/0
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let load_global_name = |unit: &&bytecode::CodeUnit| match unit.op {
Instruction::LoadGlobal { namei } => {
let name = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) >> 1;
Some(f.names[usize::try_from(name).unwrap()].as_str())
}
_ => None,
};
let value_error_pos = ops
.iter()
.position(|unit| load_global_name(unit) == Some("ValueError"))
.expect("missing ValueError load");
let key_error_pos = ops
.iter()
.position(|unit| load_global_name(unit) == Some("KeyError"))
.expect("missing KeyError load");
let first_push_exc_after_value_error = ops[value_error_pos..]
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.map(|pos| pos + value_error_pos)
.expect("missing finally exception entry");
let first_copy_after_value_error = ops[value_error_pos..]
.iter()
.position(|unit| matches!(unit.op, Instruction::Copy { .. }))
.map(|pos| pos + value_error_pos)
.expect("missing finally cleanup copy");
assert!(
first_push_exc_after_value_error < key_error_pos,
"CPython codegen_try_finally() enters the outer finally exception path before compiling the nested try body; got ops={ops:?}"
);
assert!(
first_push_exc_after_value_error < first_copy_after_value_error,
"CPython remove_unreachable() must not keep cleanup targets from dead normal-finally paths before the live exception path; got ops={ops:?}"
);
}
#[test]
fn non_simple_bare_name_annotation_does_not_create_local_binding() {
let code = compile_exec(
"\
def f2bad():
(no_such_global): int
print(no_such_global)
",
);
let f = find_code(&code, "f2bad").expect("missing f2bad code");
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadGlobal { .. })),
"expected LOAD_GLOBAL for non-simple bare annotated name, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
assert!(
!f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastCheck { .. })),
"non-simple bare annotated name should not become a local binding, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn negative_constant_binop_folds_after_unary_folding() {
let code = compile_exec(
"\
def f():
return -2147483647 - 1
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
!f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })),
"negative constant expression should fold to a single constant, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadConst { .. })),
"expected folded constant load, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn genexpr_filter_header_uses_store_fast_load_fast() {
let code = compile_exec(
"\
def f(it):
return (x for x in it if x)
",
);
let genexpr = find_code(&code, "<genexpr>").expect("missing <genexpr> code");
let store_fast_load_fast_idx = genexpr
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::StoreFastLoadFast { .. }))
.expect("missing STORE_FAST_LOAD_FAST in genexpr header");
assert!(
matches!(
genexpr
.instructions
.get(store_fast_load_fast_idx + 1)
.map(|unit| unit.op),
Some(Instruction::ToBool)
),
"expected TO_BOOL immediately after STORE_FAST_LOAD_FAST, got ops={:?}",
genexpr
.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn generator_filter_keeps_cpython_style_forward_yield_body_entry() {
let code = compile_exec(
"\
def gen(it):
for f in it:
if f.name:
yield f.name
",
);
let gen_code = find_code(&code, "gen").expect("missing gen code");
let ops: Vec<_> = gen_code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::YieldValue { .. },
]
)
}),
"expected CPython-style generator filter to jump on true into the yield body and fall through into the loop backedge on false, got ops={ops:?}"
);
}
#[test]
fn generator_negated_filter_keeps_cpython_style_false_edge_into_yield_body() {
let code = compile_exec(
"\
def gen(fields):
for f in fields:
if f.init and not f.kw_only:
yield f
",
);
let gen_code = find_code(&code, "gen").expect("missing gen code");
let ops: Vec<_> = gen_code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::YieldValue { .. },
Instruction::Resume { .. },
]
)
}),
"expected CPython-style negated generator filter to jump on false into the yield body and fall through into the loop backedge on true, got ops={ops:?}"
);
}
#[test]
fn loop_filter_with_nested_loop_body_uses_cpython_implicit_continue_layout() {
let code = compile_exec(
"\
def f(values):
for fmt, items in values:
nd = make(items, fmt)
for i in range(-5, 5):
check(nd[i], items[i])
check(nd[-6])
check(nd[5])
if is_memoryview_format(fmt):
mv = memoryview(nd)
check(mv, nd)
for i in range(-5, 5):
check(mv[i], items[i])
check(mv[-6])
check(mv[5])
return None
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadGlobal { .. },
]
)
}),
"expected CPython-style nested loop filter to fall through into the implicit continue and jump on true into the body, got ops={ops:?}"
);
}
#[test]
fn final_elif_with_inlined_comprehensions_threads_backedge_before_body() {
let code = compile_exec(
"\
def f(checks, enumeration, named):
for check in checks:
if check == 1:
pass
elif check is named:
member_names = enumeration._member_names_
member_values = [m.value for m in enumeration]
missing_names = []
missing_value = 0
for name, alias in enumeration._member_map_.items():
if name in member_names:
continue
if alias.value < 0:
continue
values = list(_iter_bits_lsb(alias.value))
missed = [v for v in values if v not in member_values]
if missed:
missing_names.append(name)
for val in missed:
missing_value |= val
if missing_names:
raise ValueError('x')
return enumeration
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::IsOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"expected CPython-style final elif to put loop backedge before the inlined-comprehension body, got ops={ops:?}"
);
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::IsOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"unexpected final elif body before loop backedge, got ops={ops:?}"
);
}
#[test]
fn multi_with_header_uses_store_fast_load_fast() {
let code = compile_exec(
"\
def f(manager):
with manager() as x, manager():
pass
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::StoreFastLoadFast { .. })),
"expected STORE_FAST_LOAD_FAST in multi-with header, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn sequential_store_then_load_uses_store_fast_load_fast() {
let code = compile_exec(
"\
def f(self):
x = ''; y = \"\"; self.assertTrue(len(x) == 0 and x == y)
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::StoreFastLoadFast { .. })),
"expected STORE_FAST_LOAD_FAST in sequential statement body, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn match_guard_capture_uses_store_fast_load_fast() {
let code = compile_exec(
"\
def f():
match 0:
case x if x:
z = 0
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::StoreFastLoadFast { .. })),
"expected STORE_FAST_LOAD_FAST in match guard capture path, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn match_nested_capture_uses_store_fast_store_fast() {
let code = compile_exec(
"\
def f(x):
match x:
case ((0 as w) as z):
return w, z
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::StoreFastStoreFast { .. })),
"expected STORE_FAST_STORE_FAST in nested match capture path, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn match_value_real_zero_minus_zero_complex_folds_to_negative_zero_imag() {
let code = compile_exec(
"\
def f(x):
match x:
case 0 - 0j:
return 0
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
f.constants.iter().any(|constant| matches!(
constant,
ConstantData::Complex { value }
if value.re == 0.0 && value.im == 0.0 && value.im.is_sign_negative()
)),
"expected folded -0j constant in match value"
);
}
#[test]
fn match_negative_value_const_precedes_implicit_none_like_cpython() {
let code = compile_exec(
"\
def f(x):
match x:
case -0.0:
y = 0
",
);
let f = find_code(&code, "f").expect("missing function code");
let negative_zero_index = f
.constants
.iter()
.position(|constant| {
matches!(
constant,
ConstantData::Float { value } if *value == 0.0 && value.is_sign_negative()
)
})
.expect("missing folded -0.0 match value");
let none_index = f
.constants
.iter()
.position(|constant| matches!(constant, ConstantData::None))
.expect("missing implicit None");
assert!(
negative_zero_index < none_index,
"CPython ast_preprocess.c folds MatchValue constants before codegen registers the implicit None"
);
}
#[test]
fn match_or_uses_shared_success_block() {
let code = compile_exec(
"\
def http_error(status):
match status:
case 400:
return 'Bad request'
case 401 | 403 | 404:
return 'Not allowed'
case 418:
return 'I am a teapot'
",
);
let f = find_code(&code, "http_error").expect("missing http_error code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let jump_positions: Vec<_> = ops
.iter()
.enumerate()
.filter_map(|(i, op)| matches!(op, Instruction::JumpForward { .. }).then_some(i))
.collect();
assert!(
jump_positions.len() >= 4,
"expected shared-success JumpForward ops in OR pattern, got ops={ops:?}"
);
let first_pop_top_pair = ops
.windows(2)
.position(|window| matches!(window, [Instruction::PopTop, Instruction::PopTop]))
.expect("missing POP_TOP/POP_TOP success cleanup");
assert!(
jump_positions
.iter()
.take(3)
.all(|&idx| idx < first_pop_top_pair),
"expected OR-alternative jumps before shared success cleanup, got ops={ops:?}"
);
}
#[test]
fn match_try_body_keeps_setup_nop_after_success_pop() {
let code = compile_exec(
"\
def f(x):
match x:
case 1:
try:
y = 2
except Exception:
pass
case 2:
y = 3
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| matches!(
window,
[
Instruction::PopTop,
Instruction::Nop,
Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. }
]
)),
"expected CPython-style match success POP_TOP followed by try-entry NOP, got ops={ops:?}"
);
}
#[test]
fn match_mapping_attribute_key_keeps_plain_load_fast_without_block_disable() {
let code = compile_exec(
"\
def f(self):
class Keys:
KEY = 'a'
x = {'a': 0, 'b': 1}
with self.assertRaises(ValueError):
match x:
case {Keys.KEY: y, 'a': z}:
w = 0
",
);
let f = find_code(&code, "f").expect("missing function code");
let assert_raises_attr = f
.instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "assertRaises"
}
_ => false,
})
.expect("missing assertRaises attribute load");
let assert_raises_receiver = f.instructions[assert_raises_attr - 1].op;
assert!(
matches!(assert_raises_receiver, Instruction::LoadFastBorrow { .. }),
"mapping attribute key handling must not disable borrow optimization for the whole block; got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
let key_load_idx = f
.instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "KEY"
}
_ => false,
})
.expect("missing Keys.KEY attribute load");
let prev = f.instructions[key_load_idx - 1].op;
assert!(
matches!(prev, Instruction::LoadFast { .. }),
"CPython optimize_load_fast() records MATCH_KEYS' no-input pseudo-ref with the produced-value loop index, so this consumed Keys load stays strong; got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn if_false_body_blocks_following_load_fast_borrow() {
let code = compile_exec(
"\
def f(self, groupby):
self.a()
if False:
self.dead()
self.b(groupby)
",
);
let f = find_code(&code, "f").expect("missing function code");
let units: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let b_attr_idx = units
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "b"
}
_ => false,
})
.expect("missing self.b attribute load");
assert!(
matches!(units[b_attr_idx - 1].op, Instruction::LoadFast { .. }),
"CPython keeps plain LOAD_FAST after an if False dead-body placeholder, got ops={:?}",
units.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
assert!(
matches!(units[b_attr_idx + 1].op, Instruction::LoadFast { .. }),
"CPython keeps the argument LOAD_FAST plain after an if False dead-body placeholder, got ops={:?}",
units.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
}
#[test]
fn imap_append_untagged_assert_tail_keeps_load_fast() {
let code = compile_exec(
"\
def f(self, typ, dat):
if self._idle_capture:
if (not self._idle_responses or
isinstance(self._idle_responses[-1][1][-1], bytes)):
self._idle_responses.append((typ, [dat]))
else:
response = self._idle_responses[-1]
assert response[0] == typ
response[1].append(dat)
if __debug__ and self.debug >= 5:
self._mesg(f'idle: queue untagged {typ} {dat!r}')
return
",
);
let f = find_code(&code, "f").expect("missing f code");
let units: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let debug_attr_idx = units
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "debug"
}
_ => false,
})
.expect("missing debug attribute load");
assert!(
matches!(units[debug_attr_idx - 1].op, Instruction::LoadFast { .. }),
"CPython keeps the debug tail after an emptied bool-op block as LOAD_FAST, got ops={:?}",
units.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
let mesg_attr_idx = units
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "_mesg"
}
_ => false,
})
.expect("missing _mesg attribute load");
assert!(
matches!(units[mesg_attr_idx - 1].op, Instruction::LoadFast { .. })
&& matches!(units[mesg_attr_idx + 2].op, Instruction::LoadFast { .. })
&& matches!(units[mesg_attr_idx + 5].op, Instruction::LoadFast { .. }),
"CPython keeps LOAD_FAST in the debug message after the empty join block, got ops={:?}",
units.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
}
#[test]
fn assert_success_empty_boolop_block_keeps_load_fast() {
let code = compile_exec(
"\
def f(self):
imap = self._imap
assert not imap._idle_responses
assert not imap._idle_capture
if __debug__ and imap.debug >= 4:
imap._mesg(f'idle start duration={self._duration}')
",
);
let f = find_code(&code, "f").expect("missing f code");
let units: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let debug_attr_idx = units
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "debug"
}
_ => false,
})
.expect("missing debug attribute load");
assert!(
matches!(units[debug_attr_idx - 1].op, Instruction::LoadFast { .. }),
"CPython preserves the empty assert-success bool-op block as a LOAD_FAST barrier, got ops={:?}",
units.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
}
#[test]
fn try_except_else_with_finally_keeps_with_handler_before_outer_except() {
let code = compile_exec(
"\
def f(i):
try:
1 / 0
except ZeroDivisionError:
print('e')
else:
with i as dodgy:
print('w')
finally:
print('d')
",
);
let jumpy = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = jumpy
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let with_except_idx = ops
.iter()
.position(|op| matches!(op, Instruction::WithExceptStart))
.expect("missing WITH_EXCEPT_START");
let check_exc_idx = ops
.iter()
.position(|op| matches!(op, Instruction::CheckExcMatch))
.expect("missing CHECK_EXC_MATCH");
assert!(
with_except_idx < check_exc_idx,
"expected with-except cleanup to be emitted before outer except matching like CPython, got ops={ops:?}",
);
let with_cleanup_end = ops
.windows(5)
.position(|window| {
matches!(
window,
[
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::PopTop,
]
)
})
.expect("missing with success cleanup")
+ 5;
assert!(
!matches!(ops.get(with_cleanup_end), Some(Instruction::Nop)),
"expected with success cleanup to fall straight into the surrounding continuation without a synthetic NOP target, got ops={ops:?}",
);
}
#[test]
fn nested_try_finally_keeps_inner_finally_cleanup_nop() {
let code = compile_exec(
"\
def f(a, b, d):
try:
try:
a()
finally:
b()
finally:
d()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops_lines: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, _))| {
(!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get()))
})
.collect();
assert!(
ops_lines.windows(3).any(|window| {
matches!(
window,
[
(Instruction::PopTop, 6),
(Instruction::Nop, 6),
(
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
8
),
]
)
}),
"expected CPython-style inner finally cleanup NOP before outer finalbody, got ops_lines={ops_lines:?}",
);
}
#[test]
fn nested_finally_open_conditional_falls_through_without_entry_nop() {
let code = compile_exec(
"\
def f(self, f, closed, new_key):
try:
try:
work()
finally:
if self._locked:
_unlock_file(f)
finally:
if not closed:
_sync_close(f)
return new_key
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache | Instruction::NotTaken))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
]
)
}),
"expected CPython-style fallthrough from inner finally body into outer finalbody condition, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::Nop,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
]
)
}),
"unexpected preserved inner-finally entry NOP before outer finalbody condition, got ops={ops:?}"
);
}
#[test]
fn nested_finally_closed_conditional_falls_through_without_extra_entry_nop() {
let code = compile_exec(
"\
def f(was_enabled, faulthandler, sys, orig_stderr):
try:
try:
faulthandler.enable()
faulthandler.disable()
finally:
if was_enabled:
faulthandler.enable()
else:
faulthandler.disable()
finally:
sys.stderr = orig_stderr
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache | Instruction::NotTaken))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::Nop,
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::StoreAttr { .. },
]
)
}),
"CPython keeps the inner finally cleanup anchor before the outer finalbody, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::Nop,
Instruction::Nop,
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::StoreAttr { .. },
]
)
}),
"closed conditional inner finalbody should not add a second outer finalbody-entry NOP, got ops={ops:?}"
);
}
#[test]
fn with_try_finally_normal_cleanup_keeps_redundant_jump_nop() {
let code = compile_exec(
"\
def f(cm):
with cm:
try:
x = 1
finally:
del x
return x
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops_lines: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, _))| {
(!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get()))
})
.collect();
assert!(
ops_lines.windows(6).any(|window| {
matches!(
window,
[
(Instruction::DeleteFast { .. }, 6),
(Instruction::Nop, 6),
(Instruction::LoadConst { .. }, 2),
(Instruction::LoadConst { .. }, 2),
(Instruction::LoadConst { .. }, 2),
(Instruction::Call { .. }, 2),
]
)
}),
"expected CPython-style redundant finally cleanup jump to become a line NOP before with-exit cleanup, got ops_lines={ops_lines:?}",
);
}
#[test]
fn with_try_except_normal_cleanup_keeps_body_exit_nop() {
let code = compile_exec(
"\
def f(cm, names, modname):
with cm:
try:
exec('import %s' % modname, names)
except:
raise FailedImport(modname)
return names
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::Call { .. },
Instruction::PopTop,
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"try/except inside with should preserve the CPython body-exit NOP before with cleanup, got ops={ops:?}"
);
}
#[test]
fn with_try_except_return_handler_keeps_body_exit_nop() {
let code = compile_exec(
"\
def f(cm):
with cm:
try:
x = 1
except OSError:
return False
return True
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::StoreFast { .. },
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"scope-exiting except handler inside with should preserve the CPython body-exit NOP before with cleanup, got ops={ops:?}"
);
}
#[test]
fn with_try_except_else_return_handler_keeps_body_exit_nop() {
let code = compile_exec(
"\
def f(cm, func, check):
with cm:
try:
func()
except ValueError:
return False
else:
check()
return True
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::Call { .. },
Instruction::PopTop,
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"try/except/else inside with with scope-exiting handler should preserve the CPython body-exit NOP before with cleanup, got ops={ops:?}"
);
}
#[test]
fn with_try_except_else_continue_handler_keeps_body_exit_nop() {
let code = compile_exec(
"\
def f(meta_path, cm):
for finder in meta_path:
with cm:
try:
find_spec = finder.find_spec
except AttributeError:
continue
else:
spec = find_spec()
if spec is not None:
return spec
return None
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops_lines: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, _))| {
(!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get()))
})
.collect();
assert!(
ops_lines.windows(6).any(|window| {
matches!(
window,
[
(Instruction::Call { .. }, 9),
(Instruction::StoreFast { .. }, 9),
(Instruction::Nop, 9),
(Instruction::LoadConst { .. }, 3),
(Instruction::LoadConst { .. }, 3),
(Instruction::LoadConst { .. }, 3),
]
)
}),
"try/except/else with continue handler should preserve the CPython body-exit NOP before with cleanup, got ops_lines={ops_lines:?}",
);
}
#[test]
fn elif_boolop_skips_following_elif_with_forward_jumpback_block() {
let code = compile_exec(
r#"
def f(module, fromlist, import_, recursive=False):
for x in fromlist:
if not isinstance(x, str):
raise TypeError
elif x == '*':
if not recursive and hasattr(module, '__all__'):
_handle_fromlist(module, module.__all__, import_, recursive=True)
elif not hasattr(module, x):
pass
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let is_load_global =
|unit: &&rustpython_compiler_core::bytecode::CodeUnit, name: &str| match unit.op {
Instruction::LoadGlobal { namei } => {
let namei = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) >> 1;
f.names[usize::try_from(namei).unwrap()].as_str() == name
}
_ => false,
};
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
unit0,
unit1,
unit2,
unit3,
] if matches!(unit0.op, Instruction::ToBool)
&& matches!(unit1.op, Instruction::PopJumpIfTrue { .. })
&& matches!(unit2.op, Instruction::NotTaken)
&& is_load_global(unit3, "hasattr")
)
}),
"boolop branch in elif should keep CPython's forward jump-back block before hasattr, got ops={ops:?}",
);
}
#[test]
fn with_nonterminal_try_except_normal_cleanup_drops_body_exit_nop() {
let code = compile_exec(
"\
def f(cm):
with cm:
try:
x = 1
except Exception:
pass
return x
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::StoreFast { .. },
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"non-terminal except inside with should not preserve a body-exit NOP before with cleanup, got ops={ops:?}"
);
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::StoreFast { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"expected CPython-style direct fallthrough into with cleanup, got ops={ops:?}"
);
}
#[test]
fn with_try_except_scope_exit_body_handler_fallthrough_keeps_body_exit_nop() {
let code = compile_exec(
"\
def f(cm, ValueError):
with cm:
try:
raise ValueError
except 42:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops_lines: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, _))| {
(!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get()))
})
.collect();
assert!(
ops_lines.windows(5).any(|window| {
matches!(
window,
[
(Instruction::Nop, 6),
(Instruction::LoadConst { .. }, 2),
(Instruction::LoadConst { .. }, 2),
(Instruction::LoadConst { .. }, 2),
(Instruction::Call { .. }, 2),
]
)
}),
"handler fallthrough target should preserve the CPython NOP before with cleanup, got ops_lines={ops_lines:?}",
);
}
#[test]
fn with_try_except_nested_with_normal_cleanup_drops_body_exit_nop() {
let code = compile_exec(
"\
def f(open, src, dst, copyfileobj):
with open(src) as fsrc:
try:
with open(dst) as fdst:
copyfileobj(fsrc, fdst)
except IsADirectoryError:
raise
return dst
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::Call { .. },
Instruction::PopTop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::PopTop,
]
)
}),
"nested with normal cleanup in try/except should fall directly into outer with cleanup, got ops={ops:?}"
);
assert!(
!ops.windows(8).any(|window| {
matches!(
window,
[
Instruction::Call { .. },
Instruction::PopTop,
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::PopTop,
]
)
}),
"nested with normal cleanup in try/except should not preserve a body-exit NOP before outer with cleanup, got ops={ops:?}"
);
}
#[test]
fn with_nested_if_try_except_normal_cleanup_drops_body_exit_nop() {
let code = compile_exec(
"\
def f(cm, root):
with cm:
if root:
try:
x = 1
except Exception as e:
raise ValueError from e
return root
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::StoreFast { .. },
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"nested try/except should not preserve a body-exit NOP before with cleanup, got ops={ops:?}"
);
}
#[test]
fn try_except_finally_normal_cleanup_keeps_body_exit_nop() {
let code = compile_exec(
"\
def f(self, x):
if x and self.sock:
saved = self.sock.gettimeout()
self.sock.settimeout(x)
try:
resp = self._get_line()
except TimeoutError as err:
raise self._timeout from err
finally:
self.sock.settimeout(saved)
else:
resp = self._get_line()
return resp
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let resp_store = ops
.iter()
.position(|unit| match unit.op {
Instruction::StoreFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == "resp"
}
_ => false,
})
.expect("missing resp store");
assert!(
matches!(
ops.get(resp_store + 1).map(|unit| unit.op),
Some(Instruction::Nop)
),
"expected CPython-style NOP between try/except normal body exit and finally cleanup, got ops={ops:?}",
);
}
#[test]
fn try_except_finally_open_conditional_fallthrough_drops_body_exit_nop() {
let code = compile_exec(
"\
def f(err, ov, self):
try:
if err:
assert ov
except:
ov.cancel()
raise
finally:
self.cleanup()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let assert_raise = ops
.iter()
.position(|op| matches!(op, Instruction::RaiseVarargs { .. }))
.expect("missing assertion raise");
assert!(
!matches!(ops.get(assert_raise + 1), Some(Instruction::Nop)),
"open conditional fallthrough should go directly into finally cleanup, got ops={ops:?}",
);
}
#[test]
fn try_finally_loop_fallthrough_keeps_finalbody_entry_nop() {
let code = compile_exec(
"\
def f(close, dup, first, second):
try:
retries = 0
while second != first + 1:
close(first)
retries += 1
if retries > 10:
raise RuntimeError
first, second = second, dup(second)
finally:
close(second)
close(first)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::JumpBackward { .. },
Instruction::Nop,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
Instruction::PushNull,
]
)
}),
"try/finally loop fallthrough should preserve CPython finalbody-entry NOP, got ops={ops:?}"
);
}
#[test]
fn try_finally_boolop_while_fallthrough_drops_finalbody_entry_nop() {
let code = compile_exec(
"\
def f(active, socket_map, asyncore):
try:
while active and socket_map:
asyncore.loop(timeout=0.1, count=1)
finally:
asyncore.close_all(ignore_all=True)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::JumpBackward { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"CPython removes the no-location POP_BLOCK NOP before a boolop-while try/finally finalbody, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::JumpBackward { .. },
Instruction::Nop,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"boolop-while try/finally finalbody should not keep a POP_BLOCK NOP, got ops={ops:?}"
);
}
#[test]
fn try_finally_with_infinite_loop_body_drops_finalbody_entry_nop() {
let code = compile_exec(
"\
def f(self, func, args, kwargs):
try:
with self.assertRaises(ZeroDivisionError) as cm:
while True:
self.setAlarm(self.alarm_time)
func(*args, **kwargs)
finally:
self.setAlarm(0)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::Reraise { .. },
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"CPython removes the no-location POP_BLOCK NOP before the normal finalbody after a with-wrapped infinite loop, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::Reraise { .. },
Instruction::Nop,
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"with-wrapped infinite loop try/finally should not keep a finalbody-entry NOP, got ops={ops:?}"
);
}
#[test]
fn try_finally_with_finalbody_blocks_following_with_borrow() {
let code = compile_exec(
"\
def f(self, sock, socket, HOST, OSError, TypeError):
try:
sock.bind((HOST, 0))
socket.close(sock.fileno())
with self.assertRaises(OSError):
sock.listen(1)
finally:
with self.assertRaises(OSError):
sock.close()
with self.assertRaises(TypeError):
socket.close(42, 42)
with self.assertRaises(OSError):
socket.close(-1)
",
);
let f = find_code(&code, "f").expect("missing f code");
let strong_self_loads = count_strong_loads_for_vars(f, &["self"]);
assert!(
strong_self_loads >= 2,
"CPython codegen_try_finally() emits USE_LABEL(exit) after a with finalbody, so optimize_load_fast() leaves following with receivers strong; got {strong_self_loads} strong self loads"
);
}
#[test]
fn try_finally_loop_direct_break_drops_finalbody_entry_nop() {
let code = compile_exec(
"\
def f(lines, close):
try:
while lines:
if lines[0]:
break
close(1)
finally:
close(2)
close(3)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
Instruction::PushNull,
]
)
}),
"direct loop break should enter CPython finalbody without a NOP, got ops={ops:?}",
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::Nop,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
Instruction::PushNull,
]
)
}),
"direct loop break should not preserve finalbody-entry NOP, got ops={ops:?}",
);
}
#[test]
fn try_finally_return_inside_with_pops_unwound_fblocks_for_finalbody() {
let code = compile_exec(
"\
def f(cm):
try:
with cm:
return 1
finally:
return 2
",
);
let f = find_code(&code, "f").expect("missing f code");
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::ReturnValue)),
"return inside with/try-finally should compile without leaving an invalid CFG"
);
}
#[test]
fn except_star_return_after_with_unwind_uses_no_location_like_cpython() {
let err = compile_exec_error(
"\
def f(cm):
try:
pass
except* Exception:
with cm:
return 1
",
);
assert!(matches!(
err.error,
CodegenErrorType::BreakContinueReturnInExceptStar
));
assert!(
err.location.is_none(),
"CPython codegen_unwind_fblock(WITH) sets *ploc = NO_LOCATION before the except* error"
);
}
#[test]
fn async_generator_return_value_error_message_matches_cpython() {
assert_eq!(
compile_exec_error_message(
"\
async def f():
yield 1
return 2
"
),
"'return' with value in async generator"
);
}
#[test]
fn try_except_finally_handler_normal_exit_keeps_nointerrupt_jump() {
let code = compile_exec(
"\
def f():
try:
2
except:
4
finally:
6
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(2).any(|window| {
matches!(
window,
[
Instruction::PopExcept,
Instruction::JumpBackwardNoInterrupt { .. }
]
)
}),
"CPython codegen_try_except() emits JUMP_NO_INTERRUPT to the inner end label; when wrapped by codegen_try_finally(), push_cold_blocks_to_end() preserves it as a backward no-interrupt jump, got ops={ops:?}",
);
assert!(
!ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::PopExcept,
Instruction::LoadConst { .. },
Instruction::ReturnValue,
]
)
}),
"try/except/finally handler normal exit should not inline the function epilogue over CPython's no-interrupt jump, got ops={ops:?}",
);
}
#[test]
fn nested_while_break_keeps_cpython_unreachable_end_epilogue() {
let code = compile_exec(
"\
def f():
TRUE = 1
while TRUE:
while TRUE:
break
break
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let returns = ops
.iter()
.filter(|op| matches!(op, Instruction::ReturnValue))
.count();
assert_eq!(
returns, 3,
"CPython codegen_while() emits separate anchor/end labels and codegen_break() jumps to loop->fb_exit; after redundant jump removal, the b_next return epilogue still remains, got ops={ops:?}",
);
}
#[test]
fn while_else_break_keeps_separate_continue_backedges() {
let code = compile_exec(
"\
def func():
TRUE = 1
x = [1]
while x:
x.pop()
while TRUE:
break
else:
continue
",
);
let func = find_code(&code, "func").expect("missing func code");
let ops: Vec<_> = func
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let jump_backwards = ops
.iter()
.filter(|op| matches!(op, Instruction::JumpBackward { .. }))
.count();
assert_eq!(
jump_backwards, 2,
"CPython codegen_break() emits a line-bearing jump to the inner while end, and codegen_while() keeps the else anchor separate from the end label; the break path and else-continue path should remain distinct backedges, got ops={ops:?}",
);
}
#[test]
fn break_through_finally_assert_tail_keeps_borrow_loads() {
let code = compile_exec(
"\
def func():
a, c, d, i = 1, 1, 1, 99
try:
for i in range(3):
try:
a = 5
if i > 0:
break
a = 8
finally:
c = 10
except:
d = 12
assert a == 5 and c == 10 and d == 1
",
);
let func = find_code(&code, "func").expect("missing func code");
for name in ["a", "c", "d"] {
let loads = load_fast_ops_for_var(func, name);
assert!(
loads
.iter()
.any(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"CPython flowgraph.c::optimize_load_fast() keeps assert-tail {name} loads borrowed after a resuming bare except; got loads={loads:?}",
);
}
}
#[test]
fn try_except_finally_suppressing_handler_drops_body_exit_nop() {
let code = compile_exec(
"\
def f(self):
try:
self.sock.shutdown(socket.SHUT_RDWR)
except OSError as exc:
if exc.errno != errno.ENOTCONN:
raise
finally:
self.sock.close()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let shutdown_pop = ops
.iter()
.position(|op| matches!(op, Instruction::PopTop))
.expect("missing shutdown POP_TOP");
assert!(
!matches!(ops.get(shutdown_pop + 1), Some(Instruction::Nop)),
"suppressing except handler should fall directly into finally cleanup without a CPython body-exit NOP, got ops={ops:?}",
);
assert!(
matches!(
ops.get(shutdown_pop + 1),
Some(Instruction::LoadFastBorrow { .. })
),
"suppressing except handler should keep CPython-style borrowed finally cleanup receiver, got ops={ops:?}",
);
}
#[test]
fn conditional_break_finally_does_not_keep_break_cleanup_nop() {
let code = compile_exec(
"\
def f(tar1, x):
try:
while True:
if x:
break
x = 1
finally:
tar1.close()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let ops_lines: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, _))| {
(!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get()))
})
.collect();
assert!(
!ops_lines.windows(2).any(|window| {
matches!(
window,
[
(Instruction::Nop, 5),
(
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
8
),
]
)
}),
"expected CPython-style break cleanup to jump directly into finally body, got ops_lines={ops_lines:?}",
);
let close_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "close"
}
_ => false,
})
.expect("missing close load");
assert!(
matches!(
instructions[close_attr - 1].op,
Instruction::LoadFastBorrow { .. }
),
"CPython visits the finalbody through the loop fallthrough when break is not the loop body tail; got instructions={instructions:?}",
);
}
#[test]
fn tail_conditional_break_finally_uses_empty_end_label_barrier() {
let code = compile_exec(
"\
def f(tar1, x):
try:
while True:
if x:
break
finally:
tar1.close()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let close_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "close"
}
_ => false,
})
.expect("missing close load");
assert!(
matches!(
instructions[close_attr - 1].op,
Instruction::LoadFast { .. }
),
"CPython leaves an empty while-end label before finalbody when the direct break is the loop body tail; got instructions={instructions:?}",
);
}
#[test]
fn with_break_cleanup_makes_following_jump_artificial() {
let code = compile_exec(
"\
def f(self):
while self.returncode is None:
with self._waitpid_lock:
if self.returncode is not None:
break
self.work()
return self.returncode
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops_lines: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, _))| {
(!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get()))
})
.collect();
assert!(
!ops_lines.windows(2).any(|window| {
matches!(
window,
[
(Instruction::Nop, 5),
(
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
7
),
]
)
}),
"expected CPython-style artificial jump after with-break cleanup, got ops_lines={ops_lines:?}",
);
}
#[test]
fn while_exit_before_with_cleanup_materializes_anchor_nop() {
let code = compile_exec(
"\
def f(selector, self):
with selector:
while selector.get_map():
pass
try:
self.wait()
except Exception:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops_lines: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, _))| {
(!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get()))
})
.collect();
assert!(
ops_lines.windows(6).any(|window| {
matches!(
window,
[
(Instruction::JumpBackward { .. }, 4),
(Instruction::Nop, 3),
(Instruction::LoadConst { .. }, 2),
(Instruction::LoadConst { .. }, 2),
(Instruction::LoadConst { .. }, 2),
(Instruction::Call { .. }, 2),
]
)
}),
"expected CPython-style while-exit anchor NOP before with cleanup, got ops_lines={ops_lines:?}",
);
}
#[test]
fn nested_boolop_same_or_prefixes_compile_without_extra_boolop_block() {
let code = compile_exec(
"\
def f(c, encodeO, encodeWS):
return (
(c > 127 or utf7_special[c] == 1)
or (encodeWS and (utf7_special[c] == 2))
or (encodeO and (utf7_special[c] == 3))
)
",
);
let f = find_code(&code, "f").expect("missing function code");
let pop_jump_if_true_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::PopJumpIfTrue { .. }))
.count();
assert!(
pop_jump_if_true_count >= 3,
"expected nested boolop prefix path to compile short-circuit jumps, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn nested_opposite_boolop_threads_to_fallthrough_like_cpython() {
for source in [
"\
def f(a, b, c):
return ((a and b)
or c)
",
"\
def f(a, b, c):
return ((a or b)
and c)
",
] {
let code = compile_exec(source);
let f = find_code(&code, "f").expect("missing f code");
let jumps: Vec<_> = f
.instructions
.iter()
.filter(|unit| {
matches!(
unit.op,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }
)
})
.collect();
assert_eq!(
jumps.len(),
2,
"expected two conditional jumps, got {jumps:?}"
);
assert!(
u8::from(jumps[0].arg) > u8::from(jumps[1].arg),
"expected CPython-style first jump to bypass the opposite short-circuit test, got jumps={jumps:?}"
);
}
}
#[test]
fn loop_or_continue_keeps_boolop_true_edge_to_continue() {
let code = compile_exec(
"\
def f(numpy_array, lshape, rshape, litems, fmt, tl):
for _ in range(3):
if numpy_array:
if 0 in lshape or 0 in rshape:
continue
zl = numpy_array_from_structure(litems, fmt, tl)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(8).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::LoadSmallInt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ContainsOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
]
)
}),
"expected CPython-style `or` continue test to keep first true edge to continue, got ops={ops:?}"
);
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadSmallInt { .. },
]
)
}),
"unexpected inverted first `or` continue condition before second operand, got ops={ops:?}"
);
}
#[test]
fn nested_and_or_expression_threads_same_false_short_circuit() {
let code = compile_exec(
"\
def f(fmt, MEMORYVIEW):
x = len(fmt)
return ((x == 1 or (x == 2 and fmt[0] == '@')) and
fmt[x - 1] in MEMORYVIEW)
",
);
let f = find_code(&code, "f").expect("missing f code");
let false_jumps: Vec<_> = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::PopJumpIfFalse { .. }))
.collect();
assert!(
false_jumps.len() >= 2,
"expected nested boolop false jumps, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
assert!(
u8::from(false_jumps[0].arg) > u8::from(false_jumps[1].arg),
"expected CPython-style same-false short-circuit threading to outer end, got false_jumps={false_jumps:?}"
);
}
#[test]
fn broad_exception_import_keeps_borrow_in_common_tail() {
let code = compile_exec(
"\
def f(msg):
if msg.source is not None:
try:
import tracemalloc
except Exception:
suggest_tracemalloc = False
tb = None
suggest_tracemalloc = not tracemalloc.is_tracing()
tb = tracemalloc.get_object_traceback(msg.source)
if tb is not None:
for frame in tb:
pass
return 0
",
);
let f = find_code(&code, "f").expect("missing function code");
let import_idx = f
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::ImportName { .. }))
.expect("missing IMPORT_NAME");
assert!(
f.instructions[import_idx + 1..]
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })),
"expected common tail after broad-exception import to keep LOAD_FAST_BORROW, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn try_import_return_handler_deopts_common_tail_borrow() {
let code = compile_exec(
"\
def f():
try:
import pwd, grp
except ImportError:
return False
if pwd.getpwuid(0)[0] != 'root':
return False
if grp.getgrgid(0)[0] != 'root':
return False
return True
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"expected CPython-style LOAD_FAST after protected import common tail, got ops={ops:?}",
);
}
#[test]
fn try_import_return_handler_deopts_later_protected_tail_borrow() {
let code = compile_exec(
"\
def f(info_add):
try:
import pwd
except ImportError:
return
import os
uid = os.getuid()
try:
entry = pwd.getpwuid(uid)
except KeyError:
entry = None
info_add(uid, entry)
if entry is None:
return
if hasattr(os, 'getgrouplist'):
groups = os.getgrouplist(entry.pw_name, entry.pw_gid)
groups = ', '.join(map(str, groups))
info_add('os.getgrouplist', groups)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let import_idx = ops
.iter()
.position(|op| matches!(op, Instruction::ImportName { .. }))
.expect("missing IMPORT_NAME");
let protected_tail = &ops[import_idx + 1..];
assert!(
!protected_tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"CPython keeps strong LOAD_FAST ops after protected import with return handler, even across later protected tails, got tail={protected_tail:?}"
);
}
#[test]
fn try_import_continue_handler_deopts_loop_tail_borrow() {
let code = compile_exec(
"\
def f(size):
pos = 0
while pos < size:
try:
import unicodedata
except ImportError:
continue
if pos < size:
pos += 1
return pos
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let import_idx = ops
.iter()
.position(|op| matches!(op, Instruction::ImportName { .. }))
.expect("missing IMPORT_NAME");
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_tail = &ops[import_idx + 1..handler_start];
let return_idx = normal_tail
.iter()
.position(|op| matches!(op, Instruction::ReturnValue))
.unwrap_or(normal_tail.len());
let loop_tail = &normal_tail[..return_idx.saturating_sub(1)];
assert!(
!loop_tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"CPython keeps strong LOAD_FAST ops in the loop tail after protected import with continue handler, got tail={loop_tail:?}",
);
}
#[test]
fn try_import_continue_inside_loop_keeps_earlier_loop_body_borrows() {
let code = compile_exec(
r#"
def f(s, size, errors):
p = []
pos = 0
while pos < size:
if s[pos] != ord("\\"):
p.append(chr(s[pos]))
pos += 1
continue
if pos < size:
try:
import unicodedata
except ImportError:
errors(pos, size)
continue
if pos < size:
p.append(chr(s[pos]))
pos += 1
return p, pos
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let import_idx = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::ImportName { .. }))
.expect("missing IMPORT_NAME");
let is_pair = |unit: &&CodeUnit, left_name: &str, right_name: &str, borrowed: bool| {
let Some(var_nums) = (match (unit.op, borrowed) {
(Instruction::LoadFastBorrowLoadFastBorrow { var_nums }, true)
| (Instruction::LoadFastLoadFast { var_nums }, false) => Some(var_nums),
_ => None,
}) else {
return false;
};
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == left_name
&& f.varnames[usize::from(right)] == right_name
};
let before_import = &instructions[..import_idx];
assert!(
before_import
.iter()
.any(|unit| is_pair(unit, "s", "pos", true)),
"loop body before protected import should keep CPython-style borrowed s/pos pair, got instructions={instructions:?}"
);
assert!(
!before_import
.iter()
.any(|unit| is_pair(unit, "s", "pos", false)),
"protected import later in the loop should not deopt earlier s/pos pair to strong LOAD_FAST_LOAD_FAST, got instructions={instructions:?}"
);
}
#[test]
fn try_import_pass_else_keeps_borrow() {
let code = compile_exec(
"\
def f(self):
try:
from _ctypes import set_conversion_mode
except ImportError:
pass
else:
self.prev_conv_mode = set_conversion_mode('ascii', 'strict')
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_tail = &ops[..handler_start];
assert!(
normal_tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"try-import pass/else normal path should keep CPython-style borrows, got tail={normal_tail:?}"
);
}
#[test]
fn try_import_broad_handler_implicit_return_keeps_borrow() {
let code = compile_exec(
"\
def f(self, record):
try:
import smtplib
port = self.mailport
if not port:
port = smtplib.SMTP_PORT
smtp = smtplib.SMTP(self.mailhost, port, timeout=self.timeout)
smtp.quit()
except Exception:
self.handleError(record)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_tail = &instructions[..handler_start];
let local_name = |unit: &&CodeUnit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
for name in ["self", "smtplib", "smtp"] {
assert!(
normal_tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })
&& local_name(unit) == Some(name)),
"broad except handler with implicit return should keep CPython-style borrowed {name} loads, got tail={normal_tail:?}"
);
assert!(
!normal_tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })
&& local_name(unit) == Some(name)),
"broad except handler with implicit return should not force strong {name} loads, got tail={normal_tail:?}"
);
}
}
#[test]
fn try_import_handler_assignment_resume_tail_keeps_borrow() {
let code = compile_exec(
"\
def f():
try:
import subprocess
out = subprocess.check_output(['/usr/bin/lslpp', '-Lqc', 'bos.rte'])
except ImportError:
out = _read_cmd_output('/usr/bin/lslpp -Lqc bos.rte')
out = out.decode('utf-8')
out = out.strip().split(':')
_bd = int(out[-1]) if out[-1] != '' else 9988
return (str(out[2]), _bd)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_tail = &instructions[..handler_start];
let out_idx = f
.varnames
.iter()
.position(|name| name == "out")
.expect("missing out local");
let load_out_is = |unit: &&CodeUnit, borrowed: bool| match (unit.op, borrowed) {
(Instruction::LoadFastBorrow { var_num }, true)
| (Instruction::LoadFast { var_num }, false) => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
usize::from(var_num.get(arg)) == out_idx
}
_ => false,
};
assert!(
normal_tail.iter().any(|unit| load_out_is(unit, true)),
"handler assignment resume tail should keep CPython-style borrowed out loads, got tail={normal_tail:?}"
);
assert!(
!normal_tail.iter().any(|unit| load_out_is(unit, false)),
"handler assignment resume tail should not force strong out loads, got tail={normal_tail:?}"
);
}
#[test]
fn empty_fallthrough_handler_assignment_tail_keeps_borrows() {
let code = compile_exec(
"\
def f(value):
obs_local_part = ObsLocalPart()
try:
token, value = get_word(value)
except HeaderParseError:
if value[0] not in CFWS_LEADER:
raise
token, value = get_cfws(value)
obs_local_part.append(token)
return obs_local_part, value
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_path = &ops[..handler_start];
let load_name = |unit: &&CodeUnit, name: &str, borrowed: bool| {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
match (unit.op, borrowed) {
(Instruction::LoadFastBorrow { var_num }, true)
| (Instruction::LoadFast { var_num }, false) => {
f.varnames[usize::from(var_num.get(arg))].as_str() == name
}
_ => false,
}
};
for name in ["obs_local_part", "token"] {
assert!(
normal_path.iter().any(|unit| load_name(unit, name, true)),
"handler assignment tail should keep CPython-style borrowed {name} loads, got path={normal_path:?}"
);
assert!(
!normal_path.iter().any(|unit| load_name(unit, name, false)),
"handler assignment tail should not force strong {name}, got path={normal_path:?}"
);
}
}
#[test]
fn protected_store_of_preinitialized_local_keeps_return_borrow() {
let code = compile_exec(
"\
def f(obj):
maybe_routine = obj
try:
maybe_routine = inspect.unwrap(maybe_routine)
except ValueError:
pass
return inspect.isroutine(maybe_routine)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_path = &ops[..handler_start];
let maybe_routine_idx = f
.varnames
.iter()
.position(|name| name == "maybe_routine")
.expect("missing maybe_routine local");
let loads_maybe_routine = |unit: &&CodeUnit, borrowed: bool| match (unit.op, borrowed) {
(Instruction::LoadFastBorrow { var_num }, true)
| (Instruction::LoadFast { var_num }, false) => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
usize::from(var_num.get(arg)) == maybe_routine_idx
}
_ => false,
};
assert!(
normal_path
.iter()
.any(|unit| loads_maybe_routine(unit, true)),
"preinitialized protected-store tail should keep CPython-style borrowed local, got path={normal_path:?}"
);
assert!(
!normal_path
.iter()
.any(|unit| loads_maybe_routine(unit, false)),
"preinitialized protected-store tail should not force strong local, got path={normal_path:?}"
);
}
#[test]
fn protected_attr_direct_return_keeps_borrow() {
let code = compile_exec(
"\
def f(obj):
try:
x = 1
except ValueError:
return False
return obj.values()
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let protected_tail = &ops[..handler_start];
assert!(
protected_tail.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::LoadAttr { .. },
Instruction::Call { .. },
Instruction::ReturnValue,
]
)
}),
"expected protected direct attr-call return to keep LOAD_FAST_BORROW, got tail={protected_tail:?}"
);
}
#[test]
fn protected_store_normal_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(tarfile, tarinfo, self):
try:
filtered = tarfile.tar_filter(tarinfo, '')
except UnicodeEncodeError:
return None
self.assertIs(filtered.name, tarinfo.name)
return filtered
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let filtered_store = ops
.iter()
.position(|op| matches!(op, Instruction::StoreFast { .. }))
.expect("missing filtered store");
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_tail = &ops[filtered_store + 1..handler_start];
assert!(
!normal_tail.iter().any(|op| matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)),
"expected CPython-style strong LOAD_FAST in protected store normal tail, got tail={normal_tail:?}",
);
}
#[test]
fn protected_subscript_store_normal_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(self, d, option, fallback):
try:
value = d[option]
except KeyError:
return fallback
return self.convert(value, option)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let value_store = ops
.iter()
.position(|op| matches!(op, Instruction::StoreFast { .. }))
.expect("missing value store");
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_tail = &ops[value_store + 1..handler_start];
assert!(
!normal_tail.iter().any(|op| matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)),
"expected CPython-style strong LOAD_FAST after protected subscript store, got tail={normal_tail:?}",
);
}
#[test]
fn protected_call_arm_final_store_return_uses_strong_load() {
let code = compile_exec(
"\
def f(self, action, default_metavar):
get_metavar = self._metavar_formatter(action, default_metavar)
if action.nargs is None:
result = '%s' % get_metavar(1)
elif action.nargs == OPTIONAL:
result = '[%s]' % get_metavar(1)
elif action.nargs == ZERO_OR_MORE:
metavar = get_metavar(1)
if len(metavar) == 2:
result = '[%s [%s ...]]' % metavar
else:
result = '[%s ...]' % metavar
elif action.nargs == ONE_OR_MORE:
result = '%s [%s ...]' % get_metavar(2)
elif action.nargs == REMAINDER:
result = '...'
elif action.nargs == PARSER:
result = '%s ...' % get_metavar(1)
elif action.nargs == SUPPRESS:
result = ''
else:
try:
formats = ['%s' for _ in range(action.nargs)]
except TypeError:
raise ValueError(\"invalid nargs value\") from None
result = ' '.join(formats) % get_metavar(action.nargs)
return result
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let modulo = ops
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::BinaryOp { op }
if op.get(OpArg::new(u32::from(u8::from(unit.arg))))
== BinaryOperator::Remainder
)
})
.expect("missing final format modulo");
let tail = &ops[modulo..];
assert!(
tail.windows(3).any(|window| {
matches!(
window,
[
bytecode::CodeUnit {
op: Instruction::StoreFast { .. },
..
},
bytecode::CodeUnit {
op: Instruction::LoadFast { .. },
..
},
bytecode::CodeUnit {
op: Instruction::ReturnValue,
..
},
]
)
}),
"protected call arm final store-return should keep CPython-style strong LOAD_FAST, got tail={tail:?}"
);
}
#[test]
fn protected_store_try_else_tail_keeps_borrowed_loads() {
let code = compile_exec(
"\
def f(value):
message_id = MessageID()
try:
token, value = get_msg_id(value)
message_id.append(token)
except HeaderParseError as ex:
token = get_unstructured(value)
message_id = InvalidMessageID(token)
message_id.defects.append(InvalidHeaderDefect('Invalid msg-id: {!r}'.format(ex)))
else:
if value:
message_id.defects.append(InvalidHeaderDefect('Unexpected {!r}'.format(value)))
return message_id
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_tail = &ops[..handler_start];
assert!(
normal_tail.windows(3).any(|window| {
matches!(
window,
[
CodeUnit {
op: Instruction::LoadFastBorrow { .. },
..
},
CodeUnit {
op: Instruction::ToBool,
..
},
CodeUnit {
op: Instruction::PopJumpIfFalse { .. }
| Instruction::PopJumpIfTrue { .. },
..
},
]
)
}),
"try/except/else bool guard should keep CPython-style borrowed load, got tail={normal_tail:?}",
);
let defects_idx = normal_tail
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "defects"
}
_ => false,
})
.expect("missing defects LOAD_ATTR in else tail");
assert!(
matches!(
normal_tail[defects_idx - 1].op,
Instruction::LoadFastBorrow { .. }
),
"try/except/else method receiver should stay borrowed like CPython, got tail={normal_tail:?}",
);
let format_idx = normal_tail
.iter()
.rposition(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "format"
}
_ => false,
})
.expect("missing format LOAD_ATTR in else tail");
assert!(
matches!(
normal_tail[format_idx + 1].op,
Instruction::LoadFastBorrow { .. }
),
"try/except/else format argument should stay borrowed like CPython, got tail={normal_tail:?}",
);
}
#[test]
fn nested_try_except_common_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(value):
address = Address()
try:
token, value = get_group(value)
except HeaderParseError:
try:
token, value = get_mailbox(value)
except HeaderParseError:
raise HeaderParseError('expected {}'.format(value))
address.append(token)
return address, value
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFast { .. },
Instruction::Call { .. },
Instruction::PopTop,
Instruction::LoadFastLoadFast { .. },
]
)
}),
"nested try/except common tail should use CPython-style strong loads, got ops={ops:?}"
);
}
#[test]
fn nested_try_except_branch_tail_with_following_try_uses_strong_loads() {
let code = compile_exec(
r#"
def f(value):
msg_id = MsgID()
try:
token, value = get_dot_atom_text(value)
except HeaderParseError:
try:
token, value = get_obs_local_part(value)
msg_id.defects.append(ObsoleteHeaderDefect("obsolete id-left in msg-id"))
except HeaderParseError:
raise HeaderParseError("expected {}".format(value))
msg_id.append(token)
if not value or value[0] != "@":
msg_id.defects.append(InvalidHeaderDefect("msg-id with no id-right"))
if value and value[0] == ">":
msg_id.append(ValueTerminal(">", "msg-id-end"))
value = value[1:]
return msg_id, value
msg_id.append(ValueTerminal("@", "address-at-symbol"))
value = value[1:]
try:
token, value = get_dot_atom_text(value)
except HeaderParseError:
pass
return msg_id, value
"#,
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(16).any(|window| {
matches!(
window,
[
Instruction::ReturnValue,
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadGlobal { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::Call { .. },
Instruction::PopTop,
Instruction::LoadFast { .. },
Instruction::LoadConst { .. },
Instruction::BinaryOp { .. },
Instruction::StoreFast { .. },
Instruction::Nop,
Instruction::LoadGlobal { .. },
Instruction::LoadFast { .. },
]
)
}),
"nested try branch tail before a following try should use CPython-style strong loads, got ops={ops:?}"
);
}
#[test]
fn nested_try_store_subscr_following_try_tail_uses_strong_loads() {
let code = compile_exec(
r#"
def f(value):
local_part = LocalPart()
try:
token, value = get_dot_atom(value)
except HeaderParseError:
try:
token, value = get_word(value)
except HeaderParseError:
token = TokenList()
if value:
obs_local_part, value = get_obs_local_part(str(local_part) + value)
if obs_local_part.token_type == "invalid-obs-local-part":
local_part.defects.append(InvalidHeaderDefect("invalid"))
else:
local_part.defects.append(ObsoleteHeaderDefect("obsolete"))
local_part[0] = obs_local_part
try:
local_part.value.encode("ascii")
except UnicodeEncodeError:
local_part.defects.append(NonASCIILocalPartDefect("non-ascii"))
return local_part, value
"#,
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(10).any(|window| {
matches!(
window,
[
Instruction::StoreSubscr,
Instruction::Nop,
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadAttr { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::PopTop,
Instruction::LoadFastLoadFast { .. },
Instruction::BuildTuple { .. },
]
)
}),
"nested try STORE_SUBSCR tail before a following try should use CPython-style strong loads, got ops={ops:?}"
);
}
#[test]
fn resuming_except_in_loop_keeps_post_try_store_tail_borrowed() {
let code = compile_exec(
"\
def f(part, lines, maxlen, encoding):
for name, value in part.params:
charset = encoding
error_handler = 'strict'
try:
value.encode(encoding)
encoding_required = False
except UnicodeEncodeError:
encoding_required = True
charset = 'utf-8'
if encoding_required:
encoded_value = quote(value, safe='', errors=error_handler)
tstr = \"{}*={}''{}\".format(name, charset, encoded_value)
else:
tstr = '{}={}'.format(name, quote_string(value))
if len(lines[-1]) + len(tstr) + 1 < maxlen:
lines[-1] = lines[-1] + ' ' + tstr
continue
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. },
]
)
}),
"resuming except handler should keep CPython-style borrowed bool guard, got ops={ops:?}"
);
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::LoadConst { .. },
Instruction::StoreSubscr,
]
)
}),
"resuming except handler should not deopt the post-try STORE_SUBSCR tail, got ops={ops:?}"
);
}
#[test]
fn handler_resume_loop_latch_method_call_uses_strong_loads() {
let code = compile_exec(
"\
def f(phrase, value):
while value and value[0] not in PHRASE_ENDS:
if value[0] == '.':
phrase.append(DOT)
phrase.defects.append(ObsoleteHeaderDefect('period in phrase'))
value = value[1:]
else:
try:
token, value = get_word(value)
except HeaderParseError:
if value[0] in CFWS_LEADER:
token, value = get_cfws(value)
phrase.defects.append(ObsoleteHeaderDefect('comment found without atom'))
else:
raise
phrase.append(token)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFast { .. },
Instruction::Call { .. },
Instruction::PopTop,
Instruction::JumpBackward { .. },
]
)
}) || ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFast { .. },
Instruction::Call { .. },
Instruction::PopTop,
Instruction::JumpBackward { .. },
Instruction::LoadConst { .. },
]
)
}),
"exception-handler resume loop latch should match CPython's strong method-call loads, got ops={ops:?}"
);
}
#[test]
fn single_handler_multiple_resume_branches_keep_post_try_tail_borrowed() {
let code = compile_exec(
"\
def f(part, lines, maxlen, encoding):
for name, value in part.params:
charset = encoding
error_handler = 'strict'
try:
value.encode(encoding)
encoding_required = False
except UnicodeEncodeError:
encoding_required = True
if utils._has_surrogates(value):
charset = 'unknown-8bit'
error_handler = 'surrogateescape'
else:
charset = 'utf-8'
if encoding_required:
encoded_value = quote(value, safe='', errors=error_handler)
tstr = \"{}*={}''{}\".format(name, charset, encoded_value)
else:
tstr = '{}={}'.format(name, quote_string(value))
if len(lines[-1]) + len(tstr) + 1 < maxlen:
lines[-1] = lines[-1] + ' ' + tstr
continue
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. },
]
)
}),
"single except handler with multiple resume branches should keep the post-try bool guard borrowed, got ops={ops:?}"
);
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::LoadConst { .. },
Instruction::StoreSubscr,
]
)
}),
"single except handler with multiple resume branches should keep the post-try STORE_SUBSCR tail borrowed, got ops={ops:?}"
);
}
#[test]
fn nested_exception_handler_resume_update_tail_uses_strong_load() {
let code = compile_exec(
"\
def f(inpos, size, g, replacement):
while inpos < size:
try:
g()
except KeyError:
try:
for y in replacement:
g(y)
except KeyError:
raise ValueError(inpos)
inpos += 1
return inpos
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let arg = |unit: &&bytecode::CodeUnit| OpArg::new(u32::from(u8::from(unit.arg)));
let is_inpos_load = |unit: &&bytecode::CodeUnit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
f.varnames[usize::from(var_num.get(arg(unit)))].as_str() == "inpos"
}
_ => false,
};
let is_inpos_store = |unit: &&bytecode::CodeUnit| match unit.op {
Instruction::StoreFast { var_num } => {
f.varnames[usize::from(var_num.get(arg(unit)))].as_str() == "inpos"
}
_ => false,
};
let update = ops
.windows(4)
.find(|window| {
is_inpos_load(&window[0])
&& matches!(
window[1].op,
Instruction::LoadSmallInt { i } if i.get(arg(&window[1])) == 1
)
&& matches!(
window[2].op,
Instruction::BinaryOp { op }
if op.get(arg(&window[2])) == BinaryOperator::InplaceAdd
)
&& is_inpos_store(&window[3])
})
.expect("missing inpos += 1 update");
assert!(
matches!(update[0].op, Instruction::LoadFast { .. }),
"CPython keeps a strong LOAD_FAST for nested-handler resumed inplace update, got update={update:?}"
);
}
#[test]
fn protected_store_finally_cleanup_keeps_borrow_tail() {
let code = compile_exec(
"\
def f(re, f):
try:
try:
m = re.search('x', f.read())
finally:
f.close()
if m is not None:
return m.group(1)
except OSError:
pass
return None
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let is_m_borrow = |unit: &bytecode::CodeUnit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))].as_str() == "m"
}
_ => false,
};
assert!(
ops.windows(3).any(|window| {
is_m_borrow(window[0])
&& matches!(window[1].op, Instruction::PopJumpIfNone { .. })
&& matches!(window[2].op, Instruction::NotTaken)
}) && ops.windows(2).any(|window| {
is_m_borrow(window[0]) && matches!(window[1].op, Instruction::LoadAttr { .. })
}),
"finally cleanup RERAISE should not make the outer except deopt the normal m tail, got ops={ops:?}"
);
}
#[test]
fn try_else_finally_cleanup_keeps_borrow_tail() {
let code = compile_exec(
"\
def f(re, open):
global _SYSTEM_VERSION
if _SYSTEM_VERSION is None:
_SYSTEM_VERSION = ''
try:
f = open('/System/Library/CoreServices/SystemVersion.plist', encoding='utf-8')
except OSError:
pass
else:
try:
m = re.search('x', f.read())
finally:
f.close()
if m is not None:
_SYSTEM_VERSION = '.'.join(m.group(1).split('.')[:2])
return _SYSTEM_VERSION
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let is_m_borrow = |unit: &bytecode::CodeUnit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))].as_str() == "m"
}
_ => false,
};
assert!(
ops.windows(3).any(|window| {
is_m_borrow(window[0])
&& matches!(window[1].op, Instruction::PopJumpIfNone { .. })
&& matches!(window[2].op, Instruction::NotTaken)
}) && ops.windows(2).any(|window| {
is_m_borrow(window[0]) && matches!(window[1].op, Instruction::LoadAttr { .. })
}),
"try/else finally cleanup should keep CPython-style borrowed m tail, got ops={ops:?}"
);
}
#[test]
fn typed_terminal_attr_store_deopts_later_protected_store_subscr() {
let code = compile_exec(
"\
def f(instance, self, _NOT_FOUND):
try:
cache = instance.__dict__
except AttributeError:
raise TypeError('missing dict') from None
val = cache.get(self.attrname, _NOT_FOUND)
if val is _NOT_FOUND:
val = self.func(instance)
try:
cache[self.attrname] = val
except TypeError:
raise TypeError('bad cache') from None
return val
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let store_subscr = ops
.iter()
.position(|op| matches!(op, Instruction::StoreSubscr))
.expect("missing STORE_SUBSCR");
let window = &ops[store_subscr.saturating_sub(3)..=store_subscr];
assert!(
matches!(
window,
[
Instruction::LoadFastLoadFast { .. },
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::StoreSubscr,
]
),
"CPython keeps strong loads before protected STORE_SUBSCR after a typed terminal attr-store try, got window={window:?}; ops={ops:?}"
);
}
#[test]
fn generator_protected_store_subscr_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(names, modules):
for name in names:
try:
mod = __import__(name)
except ImportError:
continue
modules[name] = mod
yield mod
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let store_subscr = ops
.iter()
.position(|op| matches!(op, Instruction::StoreSubscr))
.expect("missing STORE_SUBSCR");
let window = &ops[store_subscr.saturating_sub(2)..(store_subscr + 3).min(ops.len())];
assert!(
matches!(
window,
[
Instruction::LoadFastLoadFast { .. },
Instruction::LoadFast { .. },
Instruction::StoreSubscr,
Instruction::LoadFast { .. },
Instruction::YieldValue { .. },
..
]
),
"expected CPython-style strong LOAD_FAST around protected STORE_SUBSCR generator tail, got {window:?}"
);
}
#[test]
fn protected_call_function_ex_store_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(func, *args):
try:
result = func(*args)
except Exception:
return None
return type(result)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let tail_call = ops
.iter()
.rposition(|op| matches!(op, Instruction::Call { .. }))
.expect("missing tail CALL");
let result_store = ops[..tail_call]
.iter()
.rposition(|op| matches!(op, Instruction::StoreFast { .. }))
.expect("missing protected result STORE_FAST");
let tail = &ops[result_store + 1..tail_call];
assert!(
tail.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"expected CPython-style strong LOAD_FAST after protected CALL_FUNCTION_EX store, got ops={ops:?}",
);
assert!(
!tail
.iter()
.any(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"protected CALL_FUNCTION_EX store tail should not borrow result, got ops={ops:?}",
);
}
#[test]
fn protected_attr_subscript_tail_uses_strong_load_fast() {
let code = compile_exec(
"\
def f(obj, idx):
try:
x = 1
except ValueError:
return False
return obj.__closure__[idx]
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let protected_tail = &ops[..handler_start];
assert!(
!protected_tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected protected attr-subscript tail to keep strong LOAD_FAST ops, got tail={protected_tail:?}"
);
}
#[test]
fn protected_direct_subscript_tail_uses_strong_load_fast() {
let code = compile_exec(
"\
def f(seq):
try:
items = [int(item) for item in seq]
except ValueError:
return None
return items[0] + items[1]
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let protected_store = ops[..handler_start]
.iter()
.rposition(|op| matches!(op, Instruction::StoreFast { .. }))
.expect("missing protected local store");
let tail = &ops[protected_store + 1..handler_start];
assert!(
!tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected protected direct-subscript tail to keep strong LOAD_FAST ops, got tail={tail:?}"
);
}
#[test]
fn protected_attr_iter_chain_uses_strong_load_fast() {
let code = compile_exec(
"\
def f(fields):
try:
x = 1
except ValueError:
return False
return tuple(v for v in fields.values())
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let protected_tail = &ops[..handler_start];
assert!(
!protected_tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected protected attr-iter chain to keep strong LOAD_FAST ops, got tail={protected_tail:?}"
);
}
#[test]
fn generator_except_return_handler_deopts_normal_tail_borrows() {
let code = compile_exec(
"\
def f(fields):
try:
x = 1
except ValueError:
return
for fielddesc in fields:
yield fielddesc
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_tail = &ops[..handler_start];
assert!(
normal_tail
.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"generator tail after non-yielding except return should keep CPython-style strong LOAD_FAST, got tail={normal_tail:?}"
);
assert!(
!normal_tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"generator tail after non-yielding except return should not borrow, got tail={normal_tail:?}"
);
}
#[test]
fn generator_except_yielding_handler_keeps_normal_tail_borrows() {
let code = compile_exec(
"\
def f(tp, parent=None):
try:
fields = tp._fields_
except AttributeError:
yield parent
else:
for fielddesc in fields:
yield fielddesc
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_tail = &ops[..handler_start];
assert!(
normal_tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"generator tail after yielding except handler should keep CPython-style borrows, got tail={normal_tail:?}"
);
}
#[test]
fn generator_returning_except_keeps_yield_from_resume_tail_borrow() {
let code = compile_exec(
"\
def f(self, action):
try:
get_subactions = action._get_subactions
except AttributeError:
pass
else:
self._indent()
yield from get_subactions()
self._dedent()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let end_send = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::EndSend))
.expect("missing END_SEND");
let dedent_attr = instructions[end_send..]
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "_dedent"
}
_ => false,
})
.map(|idx| end_send + idx)
.expect("missing _dedent LOAD_ATTR");
assert!(
matches!(
instructions[dedent_attr - 1].op,
Instruction::LoadFastBorrow { .. }
),
"CPython keeps yield-from resume receiver borrowed after END_SEND, got instructions={instructions:?}"
);
}
#[test]
fn generator_except_pass_resume_tail_keeps_borrows() {
let code = compile_exec(
"\
def f(self, msg):
if self.log_queue is not None:
yield
output = []
try:
while True:
output.append(self.log_queue.get_nowait().getMessage())
except queue.Empty:
pass
else:
with self.assertLogs('concurrent.futures', 'CRITICAL') as cm:
yield
output = cm.output
self.assertTrue(any(msg in line for line in output), output)
",
);
let f = find_code(&code, "f").expect("missing f code");
let has_strong_load = |name: &str| {
f.instructions.iter().any(|unit| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
})
};
let has_borrow_load = |name: &str| {
f.instructions.iter().any(|unit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
})
};
for name in ["msg", "output"] {
assert!(
has_borrow_load(name),
"generator except-pass resume tail should borrow {name}, got instructions={:?}",
f.instructions
);
assert!(
!has_strong_load(name),
"generator except-pass resume tail should not force strong LOAD_FAST for {name}, got instructions={:?}",
f.instructions
);
}
}
#[test]
fn async_for_cleanup_resume_tail_uses_strong_loads() {
let code = compile_exec(
"\
async def f(g, self, x):
async for val in g:
break
self.x(x)
await g.aclose()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect();
let aclose_idx = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "aclose"
}
_ => false,
})
.expect("missing aclose load");
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFast { .. },
Instruction::Call { .. },
]
)
}),
"async-for cleanup resume tail should use strong LOAD_FAST ops before the await, got ops={ops:?}"
);
assert!(
matches!(
instructions
.get(aclose_idx.saturating_sub(1))
.map(|unit| unit.op),
Some(Instruction::LoadFast { .. })
),
"async-for cleanup resume tail should keep g strong before aclose, got ops={ops:?}"
);
}
#[test]
fn async_generator_async_with_yield_keeps_borrow() {
let code = compile_exec(
"\
async def f(self, my_cm):
async with self.exit_stack() as stack:
await stack.enter_async_context(my_cm())
yield stack
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect();
let wrap_idx = instructions
.iter()
.position(|unit| match unit.op {
Instruction::CallIntrinsic1 { func } => {
func.get(OpArg::new(u32::from(u8::from(unit.arg))))
== IntrinsicFunction1::AsyncGenWrap
}
_ => false,
})
.expect("missing async generator wrap");
assert!(
matches!(
ops.get(wrap_idx.saturating_sub(1)),
Some(Instruction::LoadFastBorrow { .. })
),
"async generator yield inside async-with should borrow stack like CPython, got ops={ops:?}"
);
}
#[test]
fn deoptimized_async_with_enter_continuation_uses_strong_loads() {
let code = compile_exec(
"\
async def f():
async def cm():
pass
try:
async with cm():
1 / 0
except ZeroDivisionError as e:
frames = e
class E(RuntimeError):
pass
try:
async with cm():
raise E(42)
except E as e:
frames = e
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadFast { .. },
Instruction::PushNull,
Instruction::LoadSmallInt { .. },
Instruction::Call { .. },
Instruction::RaiseVarargs { .. },
]
)
}),
"async-with enter continuation after a deoptimized setup block should keep raised class strong, got ops={ops:?}"
);
}
#[test]
fn async_with_bare_raise_continuation_keeps_borrow() {
let code = compile_exec(
"\
async def f(tg):
class E(Exception):
pass
try:
async with tg:
raise E
except ExceptionGroup:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let raise_idx = ops
.iter()
.position(|op| matches!(op, Instruction::RaiseVarargs { .. }))
.expect("missing raise");
assert!(
matches!(
ops.get(raise_idx.saturating_sub(1)),
Some(Instruction::LoadFastBorrow { .. })
),
"bare async-with raise continuation should keep the raised class borrowed like CPython, got ops={ops:?}"
);
}
#[test]
fn except_star_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(self):
try:
pass
except* ValueError:
pass
self.fail('x')
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let fail_attr = ops
.iter()
.position(|op| matches!(op, Instruction::LoadAttr { .. }))
.expect("missing self.fail load");
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"except* tail should use strong LOAD_FAST like CPython, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::LoadAttr { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"except* tail should not borrow the receiver after the handler region, got ops={ops:?}"
);
assert!(
!matches!(
ops.get(fail_attr.saturating_sub(2)),
Some(Instruction::JumpForward { .. })
),
"except* end label should not compile as an extra jump before the continuation, got ops={ops:?}"
);
}
#[test]
fn protected_attr_subscript_store_tail_uses_strong_load_fast() {
let code = compile_exec(
"\
def f(f, oldcls, newcls):
try:
idx = f.__code__.co_freevars.index('__class__')
except ValueError:
return False
closure = f.__closure__[idx]
if closure.cell_contents is oldcls:
closure.cell_contents = newcls
return True
return False
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let protected_tail = &ops[..handler_start];
let store_closure_idx = protected_tail
.windows(2)
.position(|window| {
matches!(
window,
[Instruction::BinaryOp { .. }, Instruction::StoreFast { .. }]
)
})
.map(|idx| idx + 1)
.expect("missing STORE_FAST for closure");
let post_store_tail = &protected_tail[store_closure_idx + 1..];
assert!(
!post_store_tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected protected attr-subscript store tail to keep strong LOAD_FAST ops, got tail={post_store_tail:?}"
);
}
#[test]
fn plain_attr_subscript_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self, name):
annotations = self.method_annotations[name]
return annotations
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFastBorrow { .. },
Instruction::BinaryOp { .. },
]
)
}),
"expected plain attr-subscript tail to keep borrowed receiver/index loads, got ops={ops:?}"
);
}
#[test]
fn plain_attr_iter_chain_keeps_borrow() {
let code = compile_exec(
"\
def f(fields):
return tuple(v for v in fields.values())
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::LoadAttr { .. },
Instruction::Call { .. },
Instruction::GetIter,
]
)
}),
"expected plain attr-iter chain to keep borrowed receiver, got ops={ops:?}"
);
}
#[test]
fn genexpr_true_filter_omits_bool_scaffolding() {
let code = compile_exec(
"\
def f(it):
return (x for x in it if True)
",
);
let genexpr = find_code(&code, "<genexpr>").expect("missing <genexpr> code");
assert!(
!genexpr.instructions.iter().any(|unit| {
matches!(unit.op, Instruction::LoadConst { .. })
&& matches!(
genexpr.constants.get(usize::from(u8::from(unit.arg))),
Some(ConstantData::Boolean { value: true })
)
}),
"constant-true filter should not load True, got ops={:?}",
genexpr
.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
assert!(
!genexpr
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::PopJumpIfTrue { .. })),
"constant-true filter should not leave POP_JUMP_IF_TRUE scaffolding, got ops={:?}",
genexpr
.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn classdictcell_uses_load_closure_path_and_borrows_after_optimize() {
let code = compile_exec(
"\
class C:
def method(self):
return 1
",
);
let class_code = find_code(&code, "C").expect("missing class code");
let store_classdictcell = class_code
.instructions
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::StoreName { namei }
if class_code.names
[namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize]
.as_str()
== "__classdictcell__"
)
})
.expect("missing STORE_NAME __classdictcell__");
assert!(
matches!(
class_code
.instructions
.get(store_classdictcell.saturating_sub(1))
.map(|unit| unit.op),
Some(Instruction::LoadFastBorrow { .. })
),
"expected LOAD_FAST_BORROW before __classdictcell__ store, got ops={:?}",
class_code
.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn explicit_class_dunder_class_store_uses_namespace_like_cpython() {
let code = compile_exec(
"\
class C:
def method(self):
return __class__
__class__ = 413
",
);
let class_code = find_code(&code, "C").expect("missing class code");
let class_name_index = class_code
.names
.iter()
.position(|name| name.as_str() == "__class__")
.expect("missing __class__ name");
assert!(class_code.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::StoreName { namei }
if namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize
== class_name_index
)
}));
assert!(!class_code.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::StoreDeref { i }
if class_code.cellvars
[usize::from(i.get(OpArg::new(u32::from(u8::from(unit.arg)))))]
.as_str()
== "__class__"
)
}));
}
#[test]
fn conditional_class_body_duplicates_no_location_exit_tail() {
let code = compile_exec(
"\
flag = False
class C:
if flag:
value = 1
",
);
let class_code = find_code(&code, "C").expect("missing class code");
let ops: Vec<_> = class_code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let return_count = ops
.iter()
.filter(|op| matches!(op, Instruction::ReturnValue))
.count();
let static_attrs_count = class_code
.instructions
.iter()
.filter(|unit| {
matches!(
unit.op,
Instruction::StoreName { namei }
if class_code.names
[namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize]
.as_str()
== "__static_attributes__"
)
})
.count();
assert_eq!(
return_count, 2,
"conditional class body should duplicate CPython no-location return tail, got ops={ops:?}"
);
assert_eq!(
static_attrs_count, 2,
"conditional class body should duplicate __static_attributes__ tail, got ops={ops:?}"
);
}
#[test]
fn class_lambda_assignment_does_not_create_classdictcell() {
let code = compile_exec(
"\
class C:
data = start = end = lambda *a: None
",
);
let class_code = find_code(&code, "C").expect("missing class code");
assert!(
!class_code.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::StoreName { namei }
if class_code.names
[namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize]
.as_str()
== "__classdictcell__"
)
}),
"lambda-only class should not create __classdictcell__, got ops={:?}",
class_code
.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn nested_function_static_attributes_are_collected() {
let code = compile_exec(
"\
class C:
def f(self):
self.x = 1
self.y = 2
self.x = 3
def g(self, obj):
self.y = 4
self.z = 5
def h(self, a):
self.u = 6
self.v = 7
obj.self = 8
",
);
let class_code = find_code(&code, "C").expect("missing class code");
assert!(
class_code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if elements
== &[
ConstantData::Str { value: "u".into() },
ConstantData::Str { value: "v".into() },
ConstantData::Str { value: "x".into() },
ConstantData::Str { value: "y".into() },
ConstantData::Str { value: "z".into() },
]
)),
"expected nested function static attributes in class consts"
);
}
#[test]
fn static_attributes_match_cpython_store_rule() {
let code = compile_exec(
"\
class C:
@staticmethod
def f():
self.x = 1
@classmethod
def g(cls):
self.y = 2
def h(obj):
obj.z = 3
tarinfo.uid = 4
def i(self):
self.a: int
self.b: int = 1
self.c += 1
del self.d
",
);
let class_code = find_code(&code, "C").expect("missing class code");
assert!(
class_code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if elements
== &[
ConstantData::Str { value: "b".into() },
ConstantData::Str { value: "x".into() },
ConstantData::Str { value: "y".into() },
]
)),
"expected only CPython-collected static attributes in class consts"
);
}
#[test]
fn decorated_class_uses_first_decorator_for_firstlineno() {
let code = compile_exec(
"\
@dec1
@dec2
class C:
pass
",
);
let class_code = find_code(&code, "C").expect("missing class code");
let store_firstlineno = class_code
.instructions
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::StoreName { namei }
if class_code.names
[namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize]
.as_str()
== "__firstlineno__"
)
})
.expect("missing STORE_NAME __firstlineno__");
let load_firstlineno = class_code
.instructions
.get(store_firstlineno.saturating_sub(1))
.expect("missing LOAD_CONST for __firstlineno__");
let expected = ConstantData::Integer {
value: BigInt::from(1),
};
assert!(
matches!(
load_firstlineno.op,
Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. }
),
"expected LOAD_SMALL_INT/LOAD_CONST before __firstlineno__, got {:?}",
load_firstlineno.op
);
if let Instruction::LoadConst { consti } = load_firstlineno.op {
let value = &class_code.constants
[consti.get(OpArg::new(u32::from(u8::from(load_firstlineno.arg))))];
assert_eq!(value, &expected);
} else {
assert_eq!(u32::from(u8::from(load_firstlineno.arg)), 1);
}
}
#[test]
fn class_firstlineno_store_uses_name_resolution() {
let code = compile_exec(
"\
def f():
__firstlineno__ = 1
class C:
nonlocal __firstlineno__
return C
",
);
let class_code = find_code(&code, "C").expect("missing class code");
assert!(
class_code
.freevars
.iter()
.any(|name| name == "__firstlineno__"),
"class should close over nonlocal __firstlineno__, got freevars={:?}",
class_code.freevars
);
assert!(
class_code.instructions.iter().any(|unit| match unit.op {
Instruction::StoreDeref { i } => {
let idx = i.get(OpArg::new(u32::from(u8::from(unit.arg)))).as_usize();
localsplus_name(class_code, idx) == Some("__firstlineno__")
}
_ => false,
}),
"CPython routes __firstlineno__ through name resolution and emits STORE_DEREF for __firstlineno__, got ops={:?} freevars={:?}",
class_code.instructions,
class_code.freevars
);
assert!(
!class_code.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::StoreName { namei }
if class_code.names
[namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize]
.as_str()
== "__firstlineno__"
)
}),
"nonlocal __firstlineno__ should not be stored with STORE_NAME, got ops={:?}",
class_code.instructions
);
}
#[test]
fn lambda_parent_qualname_includes_locals() {
let code = compile_exec(
"\
def f():
return lambda: (lambda: None)
",
);
let mut lambda_qualnames = Vec::new();
fn collect_lambda_qualnames(code: &CodeObject, out: &mut Vec<String>) {
if code.obj_name == "<lambda>" {
out.push(code.qualname.to_string());
}
for constant in code.constants.iter() {
if let ConstantData::Code { code } = constant {
collect_lambda_qualnames(code, out);
}
}
}
collect_lambda_qualnames(&code, &mut lambda_qualnames);
assert!(
lambda_qualnames
.iter()
.any(|name| name == "f.<locals>.<lambda>"),
"missing outer lambda qualname, got {lambda_qualnames:?}"
);
assert!(
lambda_qualnames
.iter()
.any(|name| name == "f.<locals>.<lambda>.<locals>.<lambda>"),
"nested lambda parent should include .<locals> like CPython, got {lambda_qualnames:?}"
);
}
#[test]
fn future_annotations_class_uses_direct_annotation_store() {
let code = compile_exec(
"\
from __future__ import annotations
class C:
x: int
",
);
let class_code = find_code(&code, "C").expect("missing class code");
assert!(
!class_code
.cellvars
.iter()
.any(|name| name.as_str() == "__conditional_annotations__"),
"future annotations should not create __conditional_annotations__ cellvar, got cellvars={:?}",
class_code.cellvars
);
let ops: Vec<_> = class_code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.iter()
.any(|op| matches!(op, Instruction::SetupAnnotations)),
"future annotations should emit SETUP_ANNOTATIONS, got ops={ops:?}"
);
assert!(
ops.iter().any(|op| matches!(op, Instruction::StoreSubscr)),
"future annotations should store directly into __annotations__, got ops={ops:?}"
);
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::BuildSet { .. })),
"future annotations should not initialize __conditional_annotations__, got ops={ops:?}"
);
}
#[test]
fn future_annotations_module_keeps_conditional_annotations_cell() {
let code = compile_exec(
"\
from __future__ import annotations
x: int = 1
",
);
assert!(
code.cellvars
.iter()
.any(|name| name.as_str() == "__conditional_annotations__"),
"module annotations should create __conditional_annotations__ cellvar, got cellvars={:?}",
code.cellvars
);
}
#[test]
fn future_annotations_conditional_class_keeps_conditional_annotations_cell() {
let code = compile_exec(
"\
from __future__ import annotations
class C:
if True:
x: int = 1
",
);
let class_code = find_code(&code, "C").expect("missing class code");
assert!(
class_code
.cellvars
.iter()
.any(|name| name.as_str() == "__conditional_annotations__"),
"conditional class annotations should create __conditional_annotations__ cellvar, got cellvars={:?}",
class_code.cellvars
);
}
#[test]
fn future_annotations_setup_precedes_docstring() {
let code = compile_exec(
"\
\"module doc\"
from __future__ import annotations
x: int = 1
class C:
\"class doc\"
x: int = 1
",
);
let module_setup = code
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::SetupAnnotations))
.expect("missing module SETUP_ANNOTATIONS");
let module_doc = code
.instructions
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::StoreName { namei }
if code.names
[namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize]
.as_str()
== "__doc__"
)
})
.expect("missing module doc store");
assert!(
module_setup < module_doc,
"module SETUP_ANNOTATIONS should precede docstring store, got instructions={:?}",
code.instructions
);
let class_code = find_code(&code, "C").expect("missing class code");
let class_setup = class_code
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::SetupAnnotations))
.expect("missing class SETUP_ANNOTATIONS");
let class_doc = class_code
.instructions
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::StoreName { namei }
if class_code.names
[namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize]
.as_str()
== "__doc__"
)
})
.expect("missing class doc store");
assert!(
class_setup < class_doc,
"class SETUP_ANNOTATIONS should precede docstring store, got instructions={:?}",
class_code.instructions
);
}
#[test]
fn optimize_two_strips_docstrings_during_preprocess() {
let code = compile_exec_with_options(
"\
\"module doc\"
def f():
\"function doc\"
return 1
class C:
\"class doc\"
x = 1
",
CompileOpts {
optimize: 2,
..CompileOpts::default()
},
);
assert!(
!code.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::StoreName { namei }
if code.names
[namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize]
.as_str()
== "__doc__"
)
}),
"module docstring should be stripped before codegen, got instructions={:?}",
code.instructions
);
let function_code = find_code(&code, "f").expect("missing function code");
assert!(
!function_code
.flags
.contains(bytecode::CodeFlags::HAS_DOCSTRING),
"function docstring should not set HAS_DOCSTRING when optimize=2"
);
let class_code = find_code(&code, "C").expect("missing class code");
assert!(
!class_code.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::StoreName { namei }
if class_code.names
[namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) as usize]
.as_str()
== "__doc__"
)
}),
"class docstring should be stripped before codegen, got instructions={:?}",
class_code.instructions
);
}
#[test]
fn future_annotations_flag_is_inherited_like_cpython() {
let code = compile_exec(
"\
from __future__ import annotations
def f():
class C:
pass
return C
",
);
assert!(code.flags.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS));
let f = find_code(&code, "f").expect("missing f code");
assert!(f.flags.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS));
let class_code = find_code(f, "C").expect("missing C code");
assert!(
class_code
.flags
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS)
);
}
#[test]
fn future_flags_from_compile_options_are_merged_like_cpython() {
let opts = CompileOpts {
future_features: bytecode::CodeFlags::FUTURE_ANNOTATIONS
| bytecode::CodeFlags::FUTURE_DIVISION,
..CompileOpts::default()
};
let code = compile_exec_with_options(
"\
x: int
def f():
pass
",
opts,
);
assert!(code.flags.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS));
assert!(code.flags.contains(bytecode::CodeFlags::FUTURE_DIVISION));
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::SetupAnnotations))
);
let f = find_code(&code, "f").expect("missing f code");
assert!(f.flags.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS));
assert!(f.flags.contains(bytecode::CodeFlags::FUTURE_DIVISION));
}
#[test]
fn future_barry_as_flufl_sets_module_and_nested_code_flags() {
let code = compile_exec(
"\
from __future__ import barry_as_FLUFL
def f():
pass
",
);
assert!(
code.flags
.contains(bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL)
);
let f = find_code(&code, "f").expect("missing f code");
assert!(f.flags.contains(bytecode::CodeFlags::FUTURE_BARRY_AS_BDFL));
}
#[test]
fn function_annotation_qualnames_include_the_annotated_function() {
let code = compile_exec(
"\
def f(x: int):
pass
class C:
def m(self, x: int):
pass
def outer():
def inner(x: int):
pass
",
);
let mut qualnames = Vec::new();
fn collect(code: &CodeObject, qualnames: &mut Vec<String>) {
for constant in code.constants.iter() {
if let ConstantData::Code { code } = constant {
if code.obj_name == "__annotate__" {
qualnames.push(code.qualname.clone());
}
collect(code.as_ref(), qualnames);
}
}
}
collect(&code, &mut qualnames);
assert_eq!(
qualnames,
[
"f.__annotate__",
"C.m.__annotate__",
"outer.<locals>.inner.__annotate__"
]
);
}
#[test]
fn relative_future_import_does_not_enable_annotations_like_cpython() {
let code = compile_exec(
"\
from .__future__ import annotations
x: int
",
);
assert!(!code.flags.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS));
}
#[test]
fn future_import_after_extra_string_is_rejected_like_cpython() {
assert_eq!(
compile_exec_error_message(
"\
\"\"\"Docstring\"\"\"
\"this is not a docstring\"
from __future__ import nested_scopes
",
),
"from __future__ imports must occur at the beginning of the file"
);
}
#[test]
fn future_braces_uses_cpython_special_error() {
assert_eq!(
compile_exec_error_message("from __future__ import braces\n"),
"not a chance"
);
}
#[test]
fn invalid_future_feature_is_checked_before_ast_preprocess_like_cpython() {
assert_eq!(
compile_exec_error_message("from __future__ import spam, annotations\nx: (y := int)\n"),
"future feature spam is not defined"
);
}
#[test]
fn allow_top_level_await_marks_module_coroutine_like_cpython() {
let opts = CompileOpts {
allow_top_level_await: true,
..CompileOpts::default()
};
let code = compile_exec_with_options("await f()\n", opts);
assert!(code.flags.contains(bytecode::CodeFlags::COROUTINE));
}
#[test]
fn allow_top_level_await_accepts_module_async_for_like_cpython() {
let opts = CompileOpts {
allow_top_level_await: true,
..CompileOpts::default()
};
let code = compile_exec_with_options(
"\
async for x in y:
pass
",
opts,
);
assert!(code.flags.contains(bytecode::CodeFlags::COROUTINE));
}
#[test]
fn annotation_scope_nested_flag_matches_cpython() {
let code = compile_exec(
"\
class C:
x: int
def outer():
class D:
y: int
",
);
let class_code = find_code(&code, "C").expect("missing C code");
let class_annotate =
find_code(class_code, "__annotate__").expect("missing class annotation code");
assert!(
!class_annotate.flags.contains(bytecode::CodeFlags::NESTED),
"module-level class annotation scope should not be nested"
);
let outer = find_code(&code, "outer").expect("missing outer code");
let nested_class = find_code(outer, "D").expect("missing nested class code");
let nested_annotate =
find_code(nested_class, "__annotate__").expect("missing nested annotation code");
assert!(
nested_annotate.flags.contains(bytecode::CodeFlags::NESTED),
"annotation scope under a nested class should be nested"
);
}
#[test]
fn function_like_parent_marks_child_nested_like_cpython() {
let code = compile_exec(
"\
x = lambda: (lambda: None)
type A[T] = T
",
);
let outer_lambda = find_code(&code, "<lambda>").expect("missing outer lambda code");
assert!(
!outer_lambda.flags.contains(bytecode::CodeFlags::NESTED),
"module-level lambda should not be nested"
);
let inner_lambda =
find_direct_child_code(outer_lambda, "<lambda>").expect("missing inner lambda code");
assert!(
inner_lambda.flags.contains(bytecode::CodeFlags::NESTED),
"lambda inside lambda should be nested"
);
let type_params =
find_code(&code, "<generic parameters of A>").expect("missing type params code");
assert!(
!type_params.flags.contains(bytecode::CodeFlags::NESTED),
"module-level type-parameter scope should not be nested"
);
let type_alias = find_direct_child_code(type_params, "A").expect("missing type alias code");
assert!(
type_alias.flags.contains(bytecode::CodeFlags::NESTED),
"type alias body inside type-parameter scope should be nested"
);
}
#[test]
fn plain_super_call_keeps_class_freevar() {
let code = compile_exec(
"\
class A:
pass
class B(A):
def method(self):
return super()
",
);
let method = find_code(&code, "method").expect("missing method code");
assert!(
method.freevars.iter().any(|name| name == "__class__"),
"plain super() must keep __class__ freevar, got freevars={:?}",
method.freevars
);
assert!(
method
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::CopyFreeVars { .. })),
"plain super() must keep COPY_FREE_VARS prelude, got ops={:?}",
method
.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn nested_class_super_does_not_create_outer_class_closure() {
let code = compile_exec(
"\
class C:
def outer(self):
class D:
def __init__(self):
super().__init__()
",
);
let outer_class = find_code(&code, "C").expect("missing outer class code");
let nested_class = find_code(&code, "D").expect("missing nested class code");
let init = find_code(&code, "__init__").expect("missing nested __init__ code");
assert!(
!outer_class.cellvars.iter().any(|name| name == "__class__"),
"nested super() must not force __class__ on outer class, got cellvars={:?}",
outer_class.cellvars
);
assert!(
nested_class.cellvars.iter().any(|name| name == "__class__"),
"nested class should own __class__ cell, got cellvars={:?}",
nested_class.cellvars
);
assert!(
init.freevars.iter().any(|name| name == "__class__"),
"method using super() should close over nested class, got freevars={:?}",
init.freevars
);
}
#[test]
fn nested_class_body_loads_outer_dunder_class_while_methods_use_own_cell() {
let code = compile_exec(
"\
class Outer:
def method(self):
class Inner:
value = __class__
def nested():
return __class__
",
);
let inner = find_code(&code, "Inner").expect("missing nested class code");
assert!(inner.cellvars.iter().any(|name| name == "__class__"));
assert!(inner.freevars.iter().any(|name| name == "__class__"));
assert!(
inner
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFromDictOrDeref { .. })),
"the class body must resolve __class__ from the enclosing method while the nested method closes over the new class cell"
);
}
#[test]
fn nested_closure_parameter_class_does_not_create_outer_class_closure() {
let code = compile_exec(
"\
class C:
def m(self):
def create_closure(__class__):
return (lambda: __class__).__closure__
",
);
let outer_class = find_code(&code, "C").expect("missing class code");
let create_closure =
find_code(&code, "create_closure").expect("missing create_closure code");
let lambda = find_code(&code, "<lambda>").expect("missing lambda code");
assert!(
!outer_class.cellvars.iter().any(|name| name == "__class__"),
"nested __class__ parameter must not force outer class cell, got cellvars={:?}",
outer_class.cellvars
);
assert!(
create_closure
.cellvars
.iter()
.any(|name| name == "__class__"),
"create_closure should own __class__ parameter cell, got cellvars={:?}",
create_closure.cellvars
);
assert!(
lambda.freevars.iter().any(|name| name == "__class__"),
"lambda should close over create_closure parameter, got freevars={:?}",
lambda.freevars
);
}
#[test]
fn chained_compare_jump_uses_single_cleanup_copy() {
let code = compile_exec(
"\
def f(code):
if not 1 <= code <= 2147483647:
raise ValueError('x')
",
);
let f = find_code(&code, "f").expect("missing function code");
let copy_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::Copy { .. }))
.count();
let pop_top_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::PopTop))
.count();
assert_eq!(copy_count, 1);
assert_eq!(pop_top_count, 1);
}
#[test]
fn yield_from_cleanup_jumps_to_shared_end_send() {
let code = compile_exec(
"\
def outer():
def inner():
yield from outer_gen
return inner
",
);
let inner = find_code(&code, "inner").expect("missing inner code");
let ops: Vec<_> = inner
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let cleanup_idx = ops
.iter()
.position(|op| matches!(op, Instruction::CleanupThrow))
.expect("missing CLEANUP_THROW");
assert!(
matches!(
ops.get(cleanup_idx + 1),
Some(Instruction::JumpBackwardNoInterrupt { .. } | Instruction::JumpForward { .. })
),
"expected CLEANUP_THROW to jump to shared END_SEND block, got ops={ops:?}"
);
assert!(
!matches!(ops.get(cleanup_idx + 1), Some(Instruction::EndSend)),
"CLEANUP_THROW should not inline END_SEND directly, got ops={ops:?}"
);
}
#[test]
fn try_except_falls_through_to_post_handler_code() {
let code = compile_exec(
"\
def f():
try:
line = 2
raise KeyError
except:
line = 5
line = 6
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let first_pop_except = ops
.iter()
.position(|op| matches!(op, Instruction::PopExcept))
.expect("missing POP_EXCEPT");
assert!(
!matches!(
ops.get(first_pop_except + 1),
Some(Instruction::JumpForward { .. })
),
"expected except body to fall through to post-handler code, got ops={ops:?}"
);
assert!(
matches!(
ops.get(first_pop_except + 1),
Some(Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. })
),
"expected line-after-except code immediately after POP_EXCEPT, got ops={ops:?}"
);
}
#[test]
fn try_finally_loop_fallthrough_pop_block_bounds_exception_table() {
let code = compile_exec(
"\
def f(os, E, data):
try:
while True:
part = os.read(3, 50000)
data += part
if not part or len(data) > 50000:
break
finally:
os.close(3)
if data:
raise E(2, 'x')
",
);
let f = find_code(&code, "f").expect("missing f code");
let raise_idx = u32::try_from(
f.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. }))
.expect("missing post-finally raise"),
)
.unwrap();
let entries = bytecode::decode_exception_table(&f.exceptiontable);
assert!(
entries
.iter()
.all(|entry| raise_idx < entry.start || raise_idx >= entry.end),
"post-finally raise should not remain protected by the try/finally table; entries={entries:?}, instructions={:?}",
f.instructions
);
}
#[test]
fn except_as_alias_cleanup_exception_table_matches_cpython() {
let code = compile_exec(
"\
def bug():
try:
1/0
except Exception as e:
tb = e.__traceback__
return tb
",
);
let bug = find_code(&code, "bug").expect("missing bug code");
let entries = bytecode::decode_exception_table(&bug.exceptiontable);
let not_taken_idx = u32::try_from(
bug.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::NotTaken))
.expect("missing NOT_TAKEN"),
)
.unwrap();
let alias_store_idx = not_taken_idx + 1;
let copy_idx = u32::try_from(
bug.instructions
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::Copy { i }
if i.get(OpArg::new(u32::from(u8::from(unit.arg)))) == 3
)
})
.expect("missing outer cleanup COPY"),
)
.unwrap();
assert!(
entries.iter().any(|entry| {
entry.start <= not_taken_idx
&& alias_store_idx < entry.end
&& entry.target == copy_idx
&& entry.depth == 1
&& entry.push_lasti
}),
"CPython codegen_try_except() stores the exception alias before the inner SETUP_CLEANUP, so NOT_TAKEN and the alias store stay covered by the outer cleanup entry; entries={entries:?}, instructions={:?}",
bug.instructions
);
}
#[test]
fn try_except_while_body_preserves_while_exit_line_nop() {
let code = compile_exec(
"\
def f(x, E):
try:
while x:
x -= 1
except E:
if not x:
return None
assert x
return x
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let assertion_error = ops
.iter()
.position(|op| matches!(op, Instruction::LoadCommonConstant { .. }))
.expect("missing assertion error load");
assert!(
ops[..assertion_error].windows(4).any(|window| {
matches!(
window,
[
Instruction::JumpBackward { .. },
Instruction::Nop,
Instruction::LoadFastBorrow { .. },
Instruction::ToBool,
]
)
}),
"try/except while body should preserve CPython while-exit NOP before following assert, got ops={ops:?}"
);
}
#[test]
fn constant_true_while_preserves_loop_line_nop() {
let code = compile_exec(
"\
def f(self, callback):
i = 1
while True:
for j in 1, 2, 5:
number = i * j
if callback:
callback(number, j)
return number, j
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let store_i = ops
.iter()
.position(|op| matches!(op, Instruction::StoreFast { .. }))
.expect("missing i store");
assert!(
matches!(ops.get(store_i + 1), Some(Instruction::Nop)),
"constant-true while should keep CPython loop-line NOP after setup, got ops={ops:?}"
);
}
#[test]
fn try_except_for_direct_break_preserves_normal_exhaustion_nop() {
let code = compile_exec(
"\
def f(xs, g, E):
try:
for x in xs:
if x:
break
g()
except E:
pass
g()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::Nop,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
]
)
}),
"try/except for-body with direct break should keep CPython normal-exhaustion NOP, got ops={ops:?}"
);
}
#[test]
fn try_except_for_without_direct_break_drops_normal_exhaustion_nop() {
let code = compile_exec(
"\
def f(xs, g, E):
try:
for x in xs:
g()
except E:
pass
g()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::Nop,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
]
)
}),
"try/except for-body without direct break should not keep a redundant normal-exhaustion NOP, got ops={ops:?}"
);
}
#[test]
fn terminal_except_before_conditional_tail_uses_strong_load() {
let code = compile_exec(
"\
def f(self, Exception):
try:
tree = self.g()
except Exception:
return False
if tree.body:
return True
return False
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let tree_load_before_body_attr = instructions.windows(2).find(|window| {
let loads_tree = match window[0].op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
f.varnames
[usize::from(var_num.get(OpArg::new(u32::from(u8::from(window[0].arg)))))]
== "tree"
}
_ => false,
};
loads_tree && matches!(window[1].op, Instruction::LoadAttr { .. })
});
assert!(
matches!(
tree_load_before_body_attr.map(|window| window[0].op),
Some(Instruction::LoadFast { .. })
),
"conditional tail after terminal except should match CPython's strong LOAD_FAST, got instructions={instructions:?}",
);
}
#[test]
fn try_except_continuation_folded_tuple_drops_operand_nop() {
let code = compile_exec(
"\
def f():
try:
import sqlite3
except ImportError:
return
attributes = ('sqlite_version',)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::StoreFast { .. },
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::StoreFast { .. },
]
)
}),
"expected CPython nop_out-style folded tuple operand NOP to be removed, got ops={ops:?}",
);
}
#[test]
fn if_else_normal_fallthrough_end_label_drops_return_anchor_nop() {
let code = compile_exec(
"\
def f(s):
if s[0] in (0o200, 0o377):
n = 0
for i in range(len(s) - 1):
n <<= 8
n += s[i + 1]
if s[0] == 0o377:
n = -(256 ** (len(s) - 1) - n)
else:
try:
s = nts(s, 'ascii', 'strict')
n = int(s.strip() or '0', 8)
except ValueError:
raise InvalidHeaderError('invalid header')
return n
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::StoreFast { .. },
Instruction::Nop,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
Instruction::ReturnValue,
]
)
}),
"normal fallthrough into an if-end final return should not keep a CPython return-anchor NOP, got ops={ops:?}",
);
}
#[test]
fn explicit_final_return_none_is_not_duplicated() {
let code = compile_exec(
"\
def f(src, dst, length, exception, bufsize):
if length == 0:
return
if length is None:
copyfileobj(src, dst, bufsize)
return
blocks, remainder = divmod(length, bufsize)
for b in range(blocks):
buf = src.read(bufsize)
if len(buf) < bufsize:
raise exception('unexpected end of data')
dst.write(buf)
if remainder != 0:
buf = src.read(remainder)
if len(buf) < remainder:
raise exception('unexpected end of data')
dst.write(buf)
return
",
);
let f = find_code(&code, "f").expect("missing f code");
let return_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::ReturnValue))
.count();
assert_eq!(
return_count,
3,
"explicit final return None should not be duplicated as a synthetic no-location epilogue, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn named_except_cleanup_keeps_jump_over_cleanup_and_next_try() {
let code = compile_exec(
r#"
def f(self):
try:
assert 0, 'msg'
except AssertionError as e:
self.assertEqual(e.args[0], 'msg')
else:
self.fail("AssertionError not raised by assert 0")
try:
assert False
except AssertionError as e:
self.assertEqual(len(e.args), 0)
else:
self.fail("AssertionError not raised by 'assert False'")
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let first_pop_except = ops
.iter()
.position(|op| matches!(op, Instruction::PopExcept))
.expect("missing POP_EXCEPT");
let window = &ops[first_pop_except..(first_pop_except + 6).min(ops.len())];
assert!(
matches!(
window,
[
Instruction::PopExcept,
Instruction::LoadConst { .. },
Instruction::StoreName { .. } | Instruction::StoreFast { .. },
Instruction::DeleteName { .. } | Instruction::DeleteFast { .. },
Instruction::JumpForward { .. },
..
]
),
"expected named except cleanup to jump over cleanup reraise block, got ops={window:?}"
);
}
#[test]
fn named_except_with_suppress_does_not_duplicate_following_with() {
let code = compile_exec(
"\
def f(StringIO, captured_output, print):
try:
raise KeyError
except KeyError as e:
with captured_output('stderr') as tbstderr:
print('x')
with captured_output('stderr') as tbstderr:
print('y')
else:
print('else')
s = StringIO()
return s
",
);
let f = find_code(&code, "f").expect("missing f code");
let load_y_count = f
.instructions
.iter()
.filter(|unit| match unit.op {
Instruction::LoadConst { consti } => {
matches!(
&f.constants
[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))],
ConstantData::Str { value } if value.as_str() == Ok("y")
)
}
_ => false,
})
.count();
assert_eq!(
load_y_count, 1,
"following with body should not be duplicated into the previous with suppress path"
);
}
#[test]
fn bare_except_deopts_post_handler_load_fast_borrow() {
let code = compile_exec(
"\
def f(self):
try:
1 / 0
except:
pass
with self.assertRaises(SyntaxError):
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let attr_idx = ops
.iter()
.position(|op| matches!(op, Instruction::LoadAttr { .. }))
.expect("missing LOAD_ATTR for assertRaises");
assert!(
matches!(ops.get(attr_idx - 1), Some(Instruction::LoadFast { .. })),
"bare except tail should deopt self to LOAD_FAST, got ops={ops:?}"
);
}
#[test]
fn bare_except_before_if_deopts_successor_load_fast_borrow() {
let code = compile_exec(
"\
def f(self, x):
try:
x = g()
except:
self.fail('raised')
if x:
self.fail('unexpected')
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let fail_loads = instructions
.iter()
.enumerate()
.filter_map(|(idx, unit)| {
let Instruction::LoadAttr { namei } = unit.op else {
return None;
};
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
(f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "fail")
.then_some(idx)
})
.collect::<Vec<_>>();
assert!(
fail_loads.len() >= 2,
"expected handler and successor fail calls, got instructions={instructions:?}"
);
assert!(
matches!(
instructions.get(fail_loads[1] - 1).map(|unit| unit.op),
Some(Instruction::LoadFast { .. })
),
"CPython codegen_try_except() sends a fallthrough bare handler through USE_LABEL(end); flowgraph.c::optimize_load_fast() stops at that empty end label before the following if, got instructions={instructions:?}"
);
}
#[test]
fn typed_except_keeps_post_handler_load_fast_borrow() {
let code = compile_exec(
"\
def f(self):
try:
1 / 0
except ZeroDivisionError:
pass
with self.assertRaises(SyntaxError):
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let attr_idx = ops
.iter()
.position(|op| matches!(op, Instruction::LoadAttr { .. }))
.expect("missing LOAD_ATTR for assertRaises");
assert!(
matches!(
ops.get(attr_idx - 1),
Some(Instruction::LoadFastBorrow { .. })
),
"typed except tail should keep LOAD_FAST_BORROW, got ops={ops:?}"
);
}
#[test]
fn bare_except_terminal_handler_store_subscr_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(g, cache, filename, mtime, result):
try:
module = g()
except:
return None
cache[filename] = (mtime, result)
return result
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let is_pair = |unit: &&CodeUnit, left_name: &str, right_name: &str| {
let Instruction::LoadFastLoadFast { var_nums } = unit.op else {
return false;
};
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == left_name
&& f.varnames[usize::from(right)] == right_name
};
let is_borrow_pair = |unit: &&CodeUnit, left_name: &str, right_name: &str| {
let Instruction::LoadFastBorrowLoadFastBorrow { var_nums } = unit.op else {
return false;
};
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == left_name
&& f.varnames[usize::from(right)] == right_name
};
assert!(
ops.iter().any(|unit| is_pair(unit, "mtime", "result"))
&& ops.iter().any(|unit| is_pair(unit, "cache", "filename")),
"CPython optimize_load_fast() stops at the empty try-end block for a terminal bare handler, got ops={ops:?}"
);
assert!(
!ops.iter()
.any(|unit| is_borrow_pair(unit, "mtime", "result")
|| is_borrow_pair(unit, "cache", "filename")),
"terminal bare handler post-try store tail should not be borrowed, got ops={ops:?}"
);
}
#[test]
fn while_true_try_else_break_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(path, prefix, self, cache, read, E, stat):
while True:
try:
st = stat(path)
except E:
path = path.dirname
else:
if st.mode:
raise E
break
if path not in cache:
cache[path] = read(path)
self.archive = path
self.prefix = prefix
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let is_pair = |unit: &&CodeUnit, left_name: &str, right_name: &str| {
let Instruction::LoadFastLoadFast { var_nums } = unit.op else {
return false;
};
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == left_name
&& f.varnames[usize::from(right)] == right_name
};
let is_borrow_pair = |unit: &&CodeUnit, left_name: &str, right_name: &str| {
let Instruction::LoadFastBorrowLoadFastBorrow { var_nums } = unit.op else {
return false;
};
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == left_name
&& f.varnames[usize::from(right)] == right_name
};
assert!(
ops.iter().any(|unit| is_pair(unit, "path", "cache"))
&& ops.iter().any(|unit| is_pair(unit, "path", "self"))
&& ops.iter().any(|unit| is_pair(unit, "prefix", "self")),
"CPython codegen_while() leaves an empty break end label that stops optimize_load_fast(), got ops={ops:?}"
);
assert!(
!ops.iter().any(|unit| is_borrow_pair(unit, "path", "cache")
|| is_borrow_pair(unit, "path", "self")
|| is_borrow_pair(unit, "prefix", "self")),
"while-true break successor should not be reached through a Rust-only fallthrough, got ops={ops:?}"
);
}
#[test]
fn except_handler_resume_return_call_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(class_cache, cls, KeyError, make, obj, lock, ctx):
try:
scls = class_cache[cls]
except KeyError:
scls = make(cls)
return scls(obj, lock, ctx)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let borrows_name = |unit: &&CodeUnit, name: &str| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name
}
_ => false,
};
let strong_loads_name = |unit: &&CodeUnit, name: &str| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
Instruction::LoadFastLoadFast { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name
}
_ => false,
};
let return_idx = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::ReturnValue))
.expect("missing return");
let tail = &ops[..return_idx];
for name in ["scls", "obj", "lock", "ctx"] {
assert!(
tail.iter().any(|unit| borrows_name(unit, name)),
"handler resume to CPython codegen_try_except() end label should keep return-call {name} borrowed, got tail={tail:?}"
);
assert!(
!tail.iter().any(|unit| strong_loads_name(unit, name)),
"handler resume return-call tail should not be separated by a Rust-only empty end block for {name}, got tail={tail:?}"
);
}
}
#[test]
fn typed_except_named_handler_closure_tail_keeps_borrows() {
let code = compile_exec(
"\
def f(self):
filename = TESTFN
ICACLS = expandvars('icacls')
try:
check_output([ICACLS, filename])
except CalledProcessError as ex:
self.skipTest('Unable to create inaccessible file')
def cleanup():
check_output([ICACLS, filename])
self.addCleanup(cleanup)
stat1 = stat(filename)
stat2 = stat(filename)
self.assertEqual(stat1, stat2)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let make_function = ops
.iter()
.position(|op| matches!(op, Instruction::MakeFunction))
.expect("missing MAKE_FUNCTION for cleanup closure");
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &ops[make_function.saturating_sub(2)..handler_start];
assert!(
tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"typed except closure continuation should be visited by optimize_load_fast(), got tail={tail:?}",
);
assert!(
!tail
.iter()
.any(|op| matches!(op, Instruction::LoadFast { .. })),
"CPython codegen_try_except() uses USE_LABEL(end), so the handler continuation should be a shared passthrough and post-handler closure/tail loads should borrow; got tail={tail:?}",
);
}
#[test]
fn named_terminal_raise_handler_keeps_return_pair_borrowed() {
let code = compile_exec(
"\
def f(factory, worker_json, test_name, stdout, E):
try:
result = factory(worker_json)
except E as exc:
raise RuntimeError(test_name, stdout, exc)
return result, stdout
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let has_borrowed_pair = instructions.iter().any(|unit| {
let Instruction::LoadFastBorrowLoadFastBorrow { var_nums } = unit.op else {
return false;
};
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == "result" && f.varnames[usize::from(right)] == "stdout"
});
assert!(
has_borrowed_pair,
"named terminal-raise handler should follow CPython codegen_try_except()/flowgraph.c cleanup reachability and keep return pair borrowed; got instructions={instructions:?}"
);
}
#[test]
fn conditional_typed_except_return_join_keeps_borrow() {
let code = compile_exec(
"\
def f(cond, obj, xs, E):
if cond:
try:
obj.m()
except E:
return 1
for x in xs:
obj.n(x)
return obj
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_tail = &instructions[..handler_start];
let load_name = |unit: &&CodeUnit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
for name in ["xs", "obj", "x"] {
assert!(
normal_tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })
&& load_name(unit) == Some(name)),
"conditional typed except return join should keep CPython-style borrowed {name} loads, got tail={normal_tail:?}"
);
assert!(
!normal_tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })
&& load_name(unit) == Some(name)),
"conditional typed except return join should not force strong {name} loads, got tail={normal_tail:?}"
);
}
}
#[test]
fn typed_except_pass_resume_store_subscr_tail_keeps_borrows() {
let code = compile_exec(
"\
def f(self, sys, KeyError):
mod_name = self.mod_name
try:
self._saved_module.append(sys.modules[mod_name])
except KeyError:
pass
sys.modules[mod_name] = self.module
return self
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let store_subscr_idx = ops
.iter()
.position(|op| matches!(op, Instruction::StoreSubscr))
.expect("missing STORE_SUBSCR tail");
assert!(
matches!(
ops.get(store_subscr_idx - 5),
Some(Instruction::LoadFastBorrow { .. })
) && matches!(
ops.get(store_subscr_idx - 3),
Some(Instruction::LoadFastBorrow { .. })
) && matches!(
ops.get(store_subscr_idx - 1),
Some(Instruction::LoadFastBorrow { .. })
),
"typed except pass tail should keep STORE_SUBSCR operands borrowed, got ops={ops:?}"
);
assert!(
matches!(
ops.get(store_subscr_idx + 1),
Some(Instruction::LoadFastBorrow { .. })
),
"typed except pass return should keep self borrowed, got ops={ops:?}"
);
}
#[test]
fn reraising_typed_except_deopts_post_handler_loads() {
let code = compile_exec(
"\
def f(x, os, self, pid, exitcode):
try:
y = 1
except RuntimeError:
raise
if x:
os._exit(exitcode)
self.wait_impl(pid, exitcode=exitcode)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let guard_idx = ops
.iter()
.position(|op| matches!(op, Instruction::ToBool))
.and_then(|idx| idx.checked_sub(1))
.expect("missing post-handler bool guard");
assert!(
matches!(ops.get(guard_idx), Some(Instruction::LoadFast { .. })),
"reraising typed except tail should deopt guard load, got ops={ops:?}"
);
let wait_idx = ops
.iter()
.position(|op| matches!(op, Instruction::CallKw { .. }))
.expect("missing wait_impl CALL_KW");
let call_args = &ops[wait_idx.saturating_sub(3)..wait_idx];
assert!(
call_args.iter().any(|op| matches!(
op,
Instruction::LoadFastLoadFast { .. } | Instruction::LoadFast { .. }
)),
"reraising typed except tail should keep strong fast loads for call args, got ops={ops:?}"
);
assert!(
!call_args.iter().any(|op| matches!(
op,
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastBorrow { .. }
)),
"reraising typed except tail should not borrow call args, got ops={ops:?}"
);
}
#[test]
fn reraising_outer_handler_keeps_explicit_raise_call_arg_borrow() {
let code = compile_exec(
"\
def f(file, os, stat, errno, self, fd):
try:
self._stat_atopen = os.fstat(fd)
try:
if stat.S_ISDIR(self._stat_atopen.st_mode):
raise IsADirectoryError(errno.EISDIR, os.strerror(errno.EISDIR), file)
except AttributeError:
pass
self.name = file
try:
os.lseek(fd, 0, SEEK_END)
except OSError as e:
if e.errno != errno.ESPIPE:
raise
except OSError:
raise
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let file_borrow_before_raise_call = instructions.windows(3).any(|window| {
let is_file_borrow = match window[0].op {
Instruction::LoadFastBorrow { var_num } => {
f.varnames
[usize::from(var_num.get(OpArg::new(u32::from(u8::from(window[0].arg)))))]
== "file"
}
_ => false,
};
is_file_borrow
&& matches!(window[1].op, Instruction::Call { .. })
&& matches!(window[2].op, Instruction::RaiseVarargs { .. })
});
assert!(
file_borrow_before_raise_call,
"outer reraising handler should not deopt explicit raise call args; CPython keeps file as LOAD_FAST_BORROW, got instructions={instructions:?}"
);
}
#[test]
fn reraising_except_loop_backedge_keeps_loop_header_borrow() {
let code = compile_exec(
"\
def f(self, tag, expect_bye):
while 1:
result = self.tagged_commands[tag]
if result is not None:
del self.tagged_commands[tag]
return result
if expect_bye:
typ = 'BYE'
bye = self.untagged_responses.pop(typ, None)
if bye is not None:
return (typ, bye)
self._check_bye()
try:
self._get_response()
except self.abort as val:
if __debug__:
if self.debug >= 1:
self.print_log()
raise
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let warm_ops: Vec<_> = instructions[..handler_start]
.iter()
.map(|unit| unit.op)
.collect();
assert!(
warm_ops.iter().any(|op| matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)),
"expected loop body before reraising handler to keep borrowed loads, got ops={warm_ops:?}"
);
assert!(
warm_ops
.iter()
.all(|op| !matches!(op, Instruction::LoadFast { .. })),
"loop backedge into reraising handler should not deopt warm loop loads, got ops={warm_ops:?}"
);
}
#[test]
fn protected_store_break_handler_deopts_bool_guard_tail() {
let code = compile_exec(
"\
def f(self, size):
parts = []
while size > 0:
try:
buf = self.sock.recv(DEFAULT_BUFFER_SIZE)
except ConnectionError:
break
if not buf:
break
self._readbuf.append(buf)
size -= len(buf)
return b''.join(parts)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let guard_bool = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::ToBool))
.expect("missing bool guard");
let store_buf = instructions[..guard_bool]
.iter()
.rposition(|unit| matches!(unit.op, Instruction::StoreFast { .. }))
.expect("missing protected STORE_FAST before bool guard");
let guard_load = instructions[store_buf + 1].op;
let append_call = instructions[store_buf + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::Call { .. }))
.map(|idx| idx + store_buf + 1)
.expect("missing append call");
let append_arg = instructions[append_call - 1].op;
assert!(
matches!(guard_load, Instruction::LoadFast { .. }),
"CPython uses strong LOAD_FAST for protected-store break guard, got ops={:?}",
instructions.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
assert!(
matches!(append_arg, Instruction::LoadFast { .. }),
"CPython uses strong LOAD_FAST for protected-store append arg, got ops={:?}",
instructions.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
}
#[test]
fn assertion_success_join_keeps_following_debug_tail_borrowed() {
let code = compile_exec(
"\
def f(self, typ, dat):
if self._idle_capture:
if self._idle_responses:
response = self._idle_responses[-1]
assert response[0] == typ
response[1].append(dat)
else:
self._idle_responses.append((typ, [dat]))
if self.debug >= 5:
self._mesg(f'idle: queue untagged {typ} {dat!r}')
return
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let debug_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "debug"
}
_ => false,
})
.expect("missing debug LOAD_ATTR");
let mesg_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "_mesg"
}
_ => false,
})
.expect("missing _mesg LOAD_ATTR");
assert!(
matches!(
instructions[debug_attr - 1].op,
Instruction::LoadFastBorrow { .. }
),
"CPython keeps LOAD_FAST_BORROW after assertion success join, got ops={:?}",
instructions.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
assert!(
matches!(
instructions[mesg_attr - 1].op,
Instruction::LoadFastBorrow { .. }
),
"CPython keeps LOAD_FAST_BORROW in assertion-success debug body, got ops={:?}",
instructions.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
}
#[test]
fn multi_protected_method_call_terminal_handler_keeps_try_body_borrows() {
let code = compile_exec(
"\
def f(self, literal):
try:
self.send(literal)
self.send(CRLF)
except OSError as val:
raise self.abort('socket error: %s' % val)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let first_send = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "send"
}
_ => false,
})
.expect("missing send LOAD_ATTR");
let first_literal = instructions[first_send + 1].op;
let second_send = instructions[first_send + 1..]
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "send"
}
_ => false,
})
.map(|idx| idx + first_send + 1)
.expect("missing second send LOAD_ATTR");
assert!(
matches!(
instructions[first_send - 1].op,
Instruction::LoadFastBorrow { .. }
),
"CPython keeps first protected send receiver borrowed, got ops={:?}",
instructions.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
assert!(
matches!(first_literal, Instruction::LoadFastBorrow { .. }),
"CPython keeps first protected send arg borrowed, got ops={:?}",
instructions.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
assert!(
matches!(
instructions[second_send - 1].op,
Instruction::LoadFastBorrow { .. }
),
"CPython keeps second protected send receiver borrowed, got ops={:?}",
instructions.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
}
#[test]
fn dunder_debug_constant_false_if_deopts_tail_borrow() {
let code = compile_exec(
"\
def f(self):
if not __debug__:
self.skipTest('need asserts, run without -O')
self.do_disassembly_test()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let attr_idx = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "do_disassembly_test"
}
_ => false,
})
.expect("missing LOAD_ATTR for do_disassembly_test");
let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect();
assert!(
matches!(ops.get(attr_idx - 1), Some(Instruction::LoadFast { .. })),
"constant-false __debug__ tail should deopt self to LOAD_FAST, got ops={ops:?}"
);
}
#[test]
fn constant_slice_folds_constant_bounds() {
let code = compile_exec(
"\
def f(obj):
return obj['a':123456789012345678901234567890]
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let folded_slice = f
.constants
.iter()
.find_map(|constant| match constant {
ConstantData::Slice { elements } => Some(elements),
_ => None,
})
.expect("missing folded slice constant");
assert!(
matches!(
folded_slice.as_ref(),
[
ConstantData::Str { .. },
ConstantData::Integer { .. },
ConstantData::None,
]
),
"expected folded slice('a', 123456789012345678901234567890, None), got {folded_slice:?}"
);
assert!(
matches!(
ops.as_slice(),
[
Instruction::Resume { .. },
Instruction::LoadFastBorrow { .. },
Instruction::LoadConst { .. },
Instruction::BinaryOp { .. },
Instruction::ReturnValue,
]
),
"expected CPython-style LOAD_CONST(slice(...)) path for constant bounds, got ops={ops:?}"
);
}
#[test]
fn negative_step_slice_uses_build_slice() {
let code = compile_exec(
"\
def f(obj):
return obj[::-1]
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
matches!(
ops.as_slice(),
[
Instruction::Resume { .. },
Instruction::LoadFastBorrow { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::BuildSlice { .. },
Instruction::BinaryOp { .. },
Instruction::ReturnValue,
]
),
"expected CPython-style BUILD_SLICE path for non-literal negative step, got ops={ops:?}"
);
}
#[test]
fn slice_none_bounds_and_build_slice_use_slice_location_like_cpython() {
let code = compile_exec(
"\
def f(obj, step):
return obj[::step]
",
);
let f = find_code(&code, "f").expect("missing function code");
let slice_positions: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, end_location))| {
let op = match unit.op {
Instruction::LoadConst { .. } => "LOAD_CONST",
Instruction::BuildSlice { .. } => "BUILD_SLICE",
_ => return None,
};
Some((
op,
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.collect();
assert_eq!(
slice_positions,
vec![
("LOAD_CONST", 2, 16, 2, 22),
("LOAD_CONST", 2, 16, 2, 22),
("BUILD_SLICE", 2, 16, 2, 22),
],
"CPython codegen_slice() emits missing bounds and BUILD_SLICE at LOC(slice)"
);
}
#[test]
fn bool_int_binop_constants_fold() {
let code = compile_exec(
"\
def f():
return False + 2, True + 2, False + False, True / 1, True & False
def g():
return False + 2
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::BinaryOp { .. })),
"expected CPython-style folded bool/int binops, got ops={ops:?}"
);
assert!(
matches!(
ops.as_slice(),
[
Instruction::Resume { .. },
Instruction::LoadConst { .. },
Instruction::ReturnValue
]
),
"expected folded constants for bool/int binops, got ops={ops:?}"
);
let g = find_code(&code, "g").expect("missing function code");
let g_ops: Vec<_> = g
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!g_ops
.iter()
.any(|op| matches!(op, Instruction::BinaryOp { .. })),
"expected top-level bool/int binop to fold, got ops={g_ops:?}"
);
}
#[test]
fn double_not_expression_folds_to_bool_conversion() {
let code = compile_exec(
"\
def f(x):
return not not x
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
matches!(
ops.as_slice(),
[
Instruction::Resume { .. },
Instruction::LoadFastBorrow { .. },
Instruction::ToBool,
Instruction::ReturnValue,
]
),
"expected CPython-style double-not bool conversion, got ops={ops:?}"
);
}
#[test]
fn tuple_bound_slice_uses_two_part_slice_path() {
let code = compile_exec(
"\
def f(obj):
return obj[(1, 2):]
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
matches!(
ops.as_slice(),
[
Instruction::Resume { .. },
Instruction::LoadFastBorrow { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::BinarySlice,
Instruction::ReturnValue,
]
),
"expected CPython-style BINARY_SLICE path for tuple lower bound, got ops={ops:?}"
);
}
#[test]
fn exception_cleanup_jump_to_return_is_inlined() {
let code = compile_exec(
"\
def f(names, cls):
try:
cls.attr = names
except:
pass
return names
",
);
let f = find_code(&code, "f").expect("missing function code");
let return_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::ReturnValue))
.count();
assert_eq!(
return_count, 2,
"expected CPython-style distinct return sites for normal and except paths"
);
}
#[test]
fn except_break_preserves_plain_jump_when_inlining_no_lineno_tail() {
let code = compile_exec(
"\
def f(compiler_so, cc_args):
strip_sysroot = True
if '-arch' in cc_args:
while True:
try:
index = compiler_so.index('-arch')
del compiler_so[index:index + 2]
except ValueError:
break
if strip_sysroot:
while True:
indices = [i for i, x in enumerate(compiler_so) if x.startswith('-isysroot')]
if not indices:
break
index = indices[0]
del compiler_so[index:index + 1]
return compiler_so
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(2).any(|window| {
matches!(
window,
[Instruction::PopExcept, Instruction::JumpBackward { .. }]
)
}) && !ops.windows(2).any(|window| {
matches!(
window,
[
Instruction::PopExcept,
Instruction::JumpBackwardNoInterrupt { .. }
]
)
}),
"except-break cleanup should preserve CPython's plain JUMP when a no-lineno tail is inlined, got ops={ops:?}"
);
}
#[test]
fn nested_with_bare_except_keeps_handler_cleanup_before_following_code() {
let code = compile_exec(
"\
def f(cm, self):
try:
with cm:
raise Exception
except:
pass
self.g()
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let outer_handler = ops
.iter()
.enumerate()
.filter_map(|(idx, op)| matches!(op, Instruction::PushExcInfo).then_some(idx))
.next_back()
.expect("missing outer handler");
assert!(
ops[outer_handler..].windows(6).any(|window| {
matches!(
window,
[
Instruction::PopExcept,
Instruction::JumpForward { .. },
Instruction::Copy { .. },
Instruction::PopExcept,
Instruction::Reraise { .. },
Instruction::LoadFast { .. },
]
)
}),
"expected CPython-style handler cleanup before following code, got ops={ops:?}"
);
}
#[test]
fn try_else_for_cleanup_drops_redundant_jump_nop() {
let code = compile_exec(
"\
def f(self, xs, ys, cm1, cm2):
for x in xs:
with self.subTest(x=x):
try:
with cm1:
self.a()
except Exception:
if x:
pass
else:
raise
else:
for y in ys:
with self.subTest(y=y):
with cm2:
self.b()
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::PopTop,
]
)
}),
"expected inner for cleanup to fall directly into surrounding with cleanup, got ops={ops:?}",
);
assert!(
!ops.windows(8).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::PopTop,
]
)
}),
"expected CPython-style removal of the redundant jump NOP after for cleanup, got ops={ops:?}",
);
}
#[test]
fn non_none_final_return_is_not_duplicated() {
let code = compile_exec(
"\
def f(p, s):
if p == '':
if s == '':
return 0
return -1
",
);
let f = find_code(&code, "f").expect("missing function code");
let minus_one_loads = f
.instructions
.iter()
.filter(|unit| {
matches!(
unit.op,
Instruction::LoadConst { consti }
if matches!(
f.constants.get(
consti
.get(OpArg::new(u32::from(u8::from(unit.arg))))
.as_usize()
),
Some(ConstantData::Integer { value }) if value == &BigInt::from(-1)
)
)
})
.count();
assert_eq!(
minus_one_loads,
1,
"expected a single final return -1 epilogue, got ops={:?}",
f.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn for_return_unary_constant_preserves_value_over_iterator_cleanup() {
let code = compile_exec(
"\
def f(xs):
for x in xs:
return -1
",
);
let f = find_code(&code, "f").expect("missing function code");
let units: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
assert!(
units.windows(4).any(|window| {
matches!(
window[0].op,
Instruction::LoadConst { .. } | Instruction::LoadSmallInt { .. }
) && matches!(
window[1].op,
Instruction::Swap { i }
if i.get(OpArg::new(u32::from(u8::from(window[1].arg)))) == 2
) && matches!(window[2].op, Instruction::PopTop)
&& matches!(window[3].op, Instruction::ReturnValue)
}),
"expected CPython-style LOAD_CONST/SWAP/POP_TOP/RETURN_VALUE cleanup, got units={units:?}"
);
}
#[test]
fn try_else_if_return_keeps_conditional_target_nop() {
let code = compile_exec(
"\
def f(cond):
try:
x = cond
except E:
pass
else:
if x:
return 1
return 2
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let has_cpython_nop_target = ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. },
Instruction::ReturnValue,
Instruction::Nop,
Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. },
Instruction::ReturnValue,
]
)
});
assert!(
has_cpython_nop_target,
"expected CPython-style try-else conditional target NOP, got ops={ops:?}"
);
}
#[test]
fn try_else_nested_if_return_drops_inner_conditional_target_nop() {
let code = compile_exec(
"\
def f(obj, Sig):
try:
sig = obj.__signature__
except AttributeError:
pass
else:
if sig is not None:
if not isinstance(sig, Sig):
raise TypeError(sig)
return sig
return obj
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::RaiseVarargs { .. },
Instruction::Nop,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
Instruction::ReturnValue,
]
)
}),
"inner try-else if target should not materialize as a NOP before the return, got ops={ops:?}"
);
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::RaiseVarargs { .. },
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
Instruction::ReturnValue,
Instruction::Nop,
]
)
}),
"expected only the outer try-else if false target after the return, got ops={ops:?}"
);
}
#[test]
fn try_else_nested_final_if_return_drops_nested_conditional_target_nop() {
let code = compile_exec(
"\
def f(cond, outer):
try:
x = cond
except E:
pass
else:
if outer:
if x:
return 1
return 2
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::ReturnValue,
Instruction::Nop,
Instruction::LoadSmallInt { .. } | Instruction::LoadConst { .. },
Instruction::ReturnValue,
]
)
}),
"nested try-else conditional target should fall through directly to following code, got ops={ops:?}"
);
}
#[test]
fn named_except_conditional_branch_duplicates_cleanup_return() {
let code = compile_exec(
"\
def f(self):
try:
raise TypeError('x')
except TypeError as e:
if '+' not in str(e):
self.fail('join() ate exception message')
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let cleanup_return_count = ops
.windows(6)
.filter(|window| {
matches!(
window,
[
Instruction::PopExcept,
Instruction::LoadConst { .. },
Instruction::StoreFast { .. } | Instruction::StoreName { .. },
Instruction::DeleteFast { .. } | Instruction::DeleteName { .. },
Instruction::LoadConst { .. },
Instruction::ReturnValue,
]
)
})
.count();
assert_eq!(
cleanup_return_count, 2,
"expected duplicated named-except cleanup return blocks, got ops={ops:?}"
);
}
#[test]
fn named_except_conditional_before_explicit_return_shares_cleanup_return() {
let code = compile_exec(
"\
def f(onerror, err, OSError):
try:
x()
except OSError as err:
if onerror is not None:
onerror(err)
return
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let cleanup_return_count = ops
.windows(6)
.filter(|window| {
matches!(
window,
[
Instruction::PopExcept,
Instruction::LoadConst { .. },
Instruction::StoreFast { .. } | Instruction::StoreName { .. },
Instruction::DeleteFast { .. } | Instruction::DeleteName { .. },
Instruction::LoadConst { .. },
Instruction::ReturnValue,
]
)
})
.count();
assert_eq!(
cleanup_return_count, 1,
"explicit return after the conditional should share the named-except cleanup return block, got ops={ops:?}"
);
}
#[test]
fn named_except_boolop_condition_shares_cleanup_return() {
let code = compile_exec(
"\
def f(self, module_name, ModuleNotFoundError):
try:
return importlib.import_module(module_name)
except ModuleNotFoundError as error:
if self._warn_on_extension_import and module_name in builtin_hashes:
logging.getLogger(__name__).warning('msg', error, exc_info=error)
return None
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let cleanup_return_count = ops
.windows(6)
.filter(|window| {
matches!(
window,
[
Instruction::PopExcept,
Instruction::LoadConst { .. },
Instruction::StoreFast { .. } | Instruction::StoreName { .. },
Instruction::DeleteFast { .. } | Instruction::DeleteName { .. },
Instruction::LoadConst { .. },
Instruction::ReturnValue,
]
)
})
.count();
assert_eq!(
cleanup_return_count, 1,
"CPython keeps a shared named-except cleanup return when multiple BoolOp false edges target the same cleanup block, got ops={ops:?}"
);
}
#[test]
fn listcomp_cleanup_tail_keeps_split_store_fast_pair() {
let code = compile_exec(
"\
def f(escaped_string, quote_types):
possible_quotes = [q for q in quote_types if q not in escaped_string]
return possible_quotes
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let pop_iter_idx = ops
.iter()
.position(|op| matches!(op, Instruction::PopIter))
.expect("missing POP_ITER");
let tail = &ops[pop_iter_idx + 1..];
assert!(
matches!(
tail,
[
Instruction::StoreFast { .. },
Instruction::StoreFast { .. },
Instruction::LoadFastBorrow { .. },
Instruction::ReturnValue,
..
]
),
"expected split STORE_FAST pair after listcomp cleanup, got ops={ops:?}"
);
}
#[test]
fn dictcomp_cleanup_tail_keeps_split_store_fast_pair() {
let code = compile_exec(
"\
def f(obj, g):
return {g(k): g(v) for k, v in obj.items()}
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let pop_iter_idx = ops
.iter()
.position(|op| matches!(op, Instruction::PopIter))
.expect("missing POP_ITER");
let tail = &ops[pop_iter_idx + 1..];
assert!(
matches!(
tail,
[
Instruction::Swap { .. },
Instruction::StoreFast { .. },
Instruction::StoreFast { .. },
Instruction::ReturnValue,
..
]
),
"expected split STORE_FAST pair after dictcomp cleanup, got ops={ops:?}"
);
}
#[test]
fn static_swap_triple_assign_keeps_store_fast_store_fast() {
let code = compile_exec(
"\
def f(x, y, z):
a, b, a = x, y, z
return a
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::Swap { .. },
Instruction::StoreFastStoreFast { .. },
Instruction::StoreFast { .. }
]
)
}),
"expected CPython-style SWAP/STORE_FAST_STORE_FAST/STORE_FAST sequence, got ops={ops:?}"
);
}
#[test]
fn static_swap_duplicate_pair_eliminates_swap() {
let code = compile_exec(
"\
def f(x, y):
a, a = x, y
return a
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.iter().any(|op| matches!(op, Instruction::Swap { .. })),
"duplicate pair assignment should statically eliminate SWAP, got ops={ops:?}"
);
assert!(
ops.windows(2).any(|window| {
matches!(window, [Instruction::StoreFast { .. }, Instruction::PopTop])
}),
"expected CPython-style STORE_FAST/POP_TOP duplicate assignment, got ops={ops:?}"
);
}
#[test]
fn static_swap_duplicate_prefix_eliminates_swap() {
let code = compile_exec(
"\
def f(x, y, z):
a, a, b = x, y, z
return a
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.iter().any(|op| matches!(op, Instruction::Swap { .. })),
"duplicate-prefix assignment should statically eliminate SWAP, got ops={ops:?}"
);
assert!(
ops.windows(2).any(|window| {
matches!(
window,
[Instruction::StoreFastStoreFast { .. }, Instruction::PopTop]
)
}),
"expected CPython-style STORE_FAST_STORE_FAST/POP_TOP duplicate prefix, got ops={ops:?}"
);
}
#[test]
fn constant_if_expression_stmt_in_loop_removes_empty_body() {
let code = compile_exec(
"\
def f(x):
while x:
0 if 1 else 0
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::LoadSmallInt { .. })),
"expected constant if-expression statement to compile away inside loop, got ops={ops:?}"
);
}
#[test]
fn if_expression_in_jump_context_skips_constant_true_arm_load() {
let code = compile_exec(
"\
def f():
a if (1 if b else c) else d
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::LoadSmallInt { .. })),
"expected jump-context if-expression to avoid materializing constant truthy arm, got ops={ops:?}"
);
}
#[test]
fn with_suppress_tail_duplicates_final_return_none() {
let code = compile_exec(
"\
def f(cm, cond):
if cond:
with cm():
pass
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let return_count = ops
.iter()
.filter(|op| matches!(op, Instruction::ReturnValue))
.count();
assert_eq!(
return_count, 3,
"expected duplicated return-none epilogues, got ops={ops:?}"
);
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::JumpBackwardNoInterrupt { .. })),
"with suppress tail should not jump back to shared return block, got ops={ops:?}"
);
}
#[test]
fn with_conditional_bare_return_keeps_return_line_nop_before_exit_cleanup() {
let code = compile_exec(
"\
def f(cm, registry, altkey):
with cm:
if registry.get(altkey):
return
registry[altkey] = 1
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(8).any(|window| {
matches!(
window,
[
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::PopTop,
Instruction::LoadConst { .. },
Instruction::ReturnValue,
]
)
}),
"expected CPython-style return-line NOP before with-exit cleanup return, got ops={ops:?}"
);
}
#[test]
fn multiline_nested_with_return_finally_keeps_inner_cleanup_anchor_nop() {
let code = compile_exec(
"\
def f(a, b, path):
try:
with cm(a) as x, \\
cm(b):
return g(x).copy()
finally:
try:
cleanup(path)
except ValueError:
pass
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::Copy { .. },
Instruction::PopExcept,
Instruction::Reraise { .. },
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
]
)
}),
"multi-line nested with return/finally cleanup should keep CPython's inner item anchor NOP before outer __exit__, got ops={ops:?}"
);
}
#[test]
fn with_return_value_uses_context_expr_location_like_cpython() {
let code = compile_exec(
"\
def f(cm, func, args, kwds):
with cm:
return func(*args, **kwds)
",
);
let f = find_code(&code, "f").expect("missing function code");
let return_positions: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::ReturnValue).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.collect();
assert_eq!(
return_positions,
vec![(2, 10, 2, 12), (2, 10, 2, 12)],
"CPython codegen_unwind_fblock(WITH) leaves RETURN_VALUE inheriting the context expression location"
);
}
#[test]
fn with_normal_cleanup_jump_uses_context_expr_location_like_cpython() {
let source = "\
with cm:
pass
x = 1
";
let mut opts = CompileOpts::default();
let source_file = SourceFileBuilder::new("source_path", source).finish();
let parsed = ruff_python_parser::parse(
source_file.source_text(),
ruff_python_parser::Mode::Module.into(),
)
.unwrap();
let mut ast = parsed.into_syntax();
opts.future_features |= preprocess::future_features(&ast);
let future_annotations = opts
.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false);
let ast = match ast {
ruff_python_ast::Mod::Module(stmts) => stmts,
_ => unreachable!(),
};
let symbol_table = SymbolTable::scan_program_with_options(
&ast,
source_file.clone(),
opts.allow_top_level_await,
opts.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS),
opts.recursion_limit,
)
.map_err(|e| e.into_codegen_error(source_file.name().to_owned()))
.unwrap();
let mut compiler =
Compiler::new_with_syntax_warning_handler(opts, source_file, "<module>", None);
compiler.compile_program(&ast, symbol_table).unwrap();
let jump_positions = compiler
.current_code_info()
.blocks
.iter()
.flat_map(|block| block.used_instructions())
.filter_map(|info| {
matches!(
info.instr,
AnyInstruction::Pseudo(PseudoInstruction::Jump { .. })
)
.then_some((
(
info.location.line.get(),
info.location.character_offset.get(),
info.end_location.line.get(),
info.end_location.character_offset.get(),
),
info.lineno_override,
))
})
.collect::<Vec<_>>();
assert!(
jump_positions
.iter()
.any(|(position, lineno_override)| *position == (1, 6, 1, 8)
&& *lineno_override != Some(ir::NO_LOCATION_OVERRIDE)),
"CPython codegen_with_inner() emits the normal-exit JUMP at LOC(context_expr), not NO_LOCATION; got {jump_positions:?}"
);
}
#[test]
fn async_with_return_value_uses_context_expr_location_like_cpython() {
let code = compile_exec(
"\
async def f(cm, func, args, kwds):
async with cm:
return await func(*args, **kwds)
",
);
let f = find_code(&code, "f").expect("missing function code");
let return_positions: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::ReturnValue).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.collect();
assert_eq!(
return_positions,
vec![(2, 16, 2, 18), (2, 16, 2, 18)],
"CPython codegen_unwind_fblock(ASYNC_WITH) leaves RETURN_VALUE inheriting the context expression location"
);
}
#[test]
fn try_finally_conditional_return_duplicates_finally_exit_return() {
let code = compile_exec(
"\
def f(flag, data, callback):
try:
if flag:
return
value = 1
finally:
if data:
callback(data)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let return_count = ops
.iter()
.filter(|op| matches!(op, Instruction::ReturnValue))
.count();
assert_eq!(
return_count, 4,
"try-finally return unwind should keep CPython-style distinct true/false finalbody exits, got ops={ops:?}"
);
}
#[test]
fn named_except_conditional_cleanup_is_inlined_per_branch() {
let code = compile_exec(
"\
def f(self, logger):
try:
work()
except A as exc:
if not self.closing:
self.fatal(exc, 'msg')
elif self.loop.get_debug():
logger.debug('closing', exc_info=True)
finally:
if self.length > -1:
self.recv()
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let cleanup_after_branch_count = ops
.windows(6)
.filter(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::PopExcept,
Instruction::LoadConst { .. },
Instruction::StoreFast { .. },
Instruction::DeleteFast { .. },
Instruction::JumpBackwardNoInterrupt { .. },
]
)
})
.count();
assert_eq!(
cleanup_after_branch_count, 2,
"named except branch exits should inline cleanup like CPython, got ops={ops:?}"
);
}
#[test]
fn try_finally_exception_path_duplicates_conditional_reraise() {
let code = compile_exec(
"\
def f(flag, callback):
try:
work()
finally:
if flag:
callback()
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let reraise_count = ops
.iter()
.filter(|op| matches!(op, Instruction::Reraise { .. }))
.count();
assert_eq!(
reraise_count, 3,
"try-finally exception finalbody should duplicate CPython no-location RERAISE exits, got ops={ops:?}"
);
}
#[test]
fn genexpr_compare_header_uses_store_fast_load_fast_like_cpython() {
let code = compile_exec(
"\
def f(it):
return (offset == (4, 10) for offset in it)
",
);
let genexpr = find_code(&code, "<genexpr>").expect("missing <genexpr> code");
let ops: Vec<_> = genexpr
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::StoreFastLoadFast { .. },
Instruction::LoadConst { .. },
Instruction::CompareOp { .. },
]
)
}),
"expected CPython-style STORE_FAST_LOAD_FAST compare header, got ops={ops:?}"
);
}
#[test]
fn fstring_adjacent_literals_are_merged() {
let code = compile_exec(
"\
def f(cls, proto):
raise TypeError(
f\"cannot pickle {cls.__name__!r} object: \"
f\"a class that defines __slots__ without \"
f\"defining __getstate__ cannot be pickled \"
f\"with protocol {proto}\"
)
",
);
let f = find_code(&code, "f").expect("missing function code");
let string_consts = f
.instructions
.iter()
.filter_map(|unit| match unit.op {
Instruction::LoadConst { consti } => {
Some(&f.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))])
}
_ => None,
})
.filter_map(|constant| match constant {
ConstantData::Str { value } => Some(value.to_string()),
_ => None,
})
.collect::<Vec<_>>();
assert!(
string_consts.iter().any(|value| {
value
== " object: a class that defines __slots__ without defining __getstate__ cannot be pickled with protocol "
}),
"expected merged trailing f-string literal, got {string_consts:?}"
);
assert!(
!string_consts.iter().any(|value| value == " object: "),
"did not expect split trailing literal, got {string_consts:?}"
);
}
#[test]
fn literal_only_fstring_statement_keeps_const_like_cpython() {
let code = compile_exec(
"\
def f():
f'''Not a docstring'''
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
f.constants.iter().any(|constant| matches!(
constant,
ConstantData::Str { value } if value.to_string() == "Not a docstring"
)),
"constant f-string statement should survive in co_consts like CPython"
);
}
#[test]
fn empty_fstring_literals_are_elided_around_interpolation() {
let code = compile_exec(
"\
def f(x):
if '' f'{x}':
return 1
return 2
",
);
let f = find_code(&code, "f").expect("missing function code");
let empty_string_loads = f
.instructions
.iter()
.filter_map(|unit| match unit.op {
Instruction::LoadConst { consti } => {
Some(&f.constants[consti.get(OpArg::new(u32::from(u8::from(unit.arg))))])
}
_ => None,
})
.filter(|constant| {
matches!(
constant,
ConstantData::Str { value } if value.is_empty()
)
})
.count();
let build_string_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::BuildString { .. }))
.count();
assert_eq!(empty_string_loads, 0);
assert_eq!(build_string_count, 0);
}
#[test]
fn large_fstring_uses_join_list_like_cpython() {
let mut source = String::from("def f(x):\n return f\"");
for _ in 0..=STACK_USE_GUIDELINE {
source.push_str("{x}");
}
source.push_str("\"\n");
let code = compile_exec(&source);
let f = find_code(&code, "f").expect("missing function code");
let build_string_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::BuildString { .. }))
.count();
let list_append_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::ListAppend { .. }))
.count();
let join_attr_count = f
.instructions
.iter()
.filter(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
load_attr.is_method()
&& f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "join"
}
_ => false,
})
.count();
assert_eq!(build_string_count, 0);
assert_eq!(
list_append_count,
usize::try_from(STACK_USE_GUIDELINE + 1).unwrap()
);
assert_eq!(join_attr_count, 1);
}
#[test]
fn large_fstring_join_scaffolding_uses_joinedstr_location_like_cpython() {
let mut source = String::from("def f(x):\n return f\"");
for _ in 0..=STACK_USE_GUIDELINE {
source.push_str("{x}");
}
source.push_str("\"\n");
let code = compile_exec(&source);
let f = find_code(&code, "f").expect("missing function code");
let fstring_end = " return ".len()
+ 3
+ 3 * usize::try_from(STACK_USE_GUIDELINE + 1).expect("guideline overflowed")
+ 1;
let expected = (2, 12, 2, fstring_end);
for (unit, (location, end_location)) in f.instructions.iter().zip(&f.locations) {
if matches!(
unit.op,
Instruction::BuildList { .. }
| Instruction::ListAppend { .. }
| Instruction::Call { .. }
) {
assert_eq!(
(
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
),
expected,
"CPython codegen_joined_str() emits join scaffolding at LOC(JoinedStr); this direct codegen path uses the parser's FString range, op={:?}",
unit.op
);
}
}
}
#[test]
fn large_power_is_not_constant_folded() {
let code = compile_exec("x = 2**100\n");
assert!(code.instructions.iter().any(|unit| match unit.op {
Instruction::BinaryOp { op } => {
op.get(OpArg::new(u32::from(u8::from(unit.arg)))) == oparg::BinaryOperator::Power
}
_ => false,
}));
}
#[test]
fn string_and_bytes_binops_constant_fold_like_cpython() {
let code = compile_exec(
"\
x = b'\\\\' + b'u1881'\n\
y = 103 * 'a' + 'x'\n",
);
assert!(
!code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })),
"unexpected runtime BINARY_OP in folded string/bytes constants: {:?}",
code.instructions
);
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Bytes { value } if value == b"\\u1881"
)));
let expected = format!("{}x", "a".repeat(103));
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Str { value }
if value.to_string() == expected
)));
}
#[test]
fn float_floor_division_constant_folds_like_cpython() {
let code = compile_exec(
"\
x = 1.0 // 0.1\n\
y = 1.0 % 0.1\n\
z = 1e300 * 1e300 * 0\n",
);
assert!(
!code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })),
"float constant floor-div/mod should fold away, got instructions={:?}",
code.instructions
);
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Float { value } if value.to_bits() == 9.0f64.to_bits()
)));
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Float { value }
if value.to_bits() == 0.09999999999999995f64.to_bits()
)));
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Float { value } if value.is_nan()
)));
}
#[test]
fn float_power_overflow_constant_does_not_fold() {
let code = compile_exec("x = 1e300 ** 2\n");
assert!(
code.instructions.iter().any(|unit| matches!(
unit.op,
Instruction::BinaryOp { op }
if op.get(OpArg::new(u32::from(u8::from(unit.arg))))
== oparg::BinaryOperator::Power
)),
"overflowing float power should stay runtime like CPython, got instructions={:?}",
code.instructions
);
}
#[test]
fn large_string_and_bytes_binops_constant_fold_like_cpython() {
let code = compile_exec(
r#"
encoded = b'\xff\xfe\x00\x00' + b'\x00\x00\x01\x00' * 1024
text = '\U00010000' * 1024
"#,
);
assert!(
!code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })),
"large safe string/bytes constants should fold away, got instructions={:?}",
code.instructions
);
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Bytes { value } if value.len() == 4100
)));
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Str { value } if value.code_points().count() == 1024
)));
}
#[test]
fn constant_string_subscript_folds_inside_collection() {
let code = compile_exec(
"\
values = [item for item in [r\"\\\\'a\\\\'\", r\"\\t3\", r\"\\\\\"[0]]]\n",
);
assert!(
!code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })),
"unexpected runtime BINARY_OP after constant subscript folding: {:?}",
code.instructions
);
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if elements.len() == 3
&& matches!(&elements[2], ConstantData::Str { value } if value.to_string() == "\\")
)));
}
#[test]
fn constant_string_subscript_with_surrogate_skips_lossy_fold() {
let code = compile_exec("value = \"\\ud800\"[0]\n");
assert!(
code.instructions.iter().any(|unit| match unit.op {
Instruction::BinaryOp { op } => {
op.get(OpArg::new(u32::from(u8::from(unit.arg))))
== oparg::BinaryOperator::Subscr
}
_ => false,
}),
"expected runtime subscript for surrogate literal, got instructions={:?}",
code.instructions
);
}
#[test]
fn constant_subscript_folds_in_load_context() {
let cases = [
("value = (1, 2, 3)[0]\n", Some(BigInt::from(1)), None),
("value = b\"abc\"[0]\n", Some(BigInt::from(97)), None),
("value = \"abc\"[0]\n", None, Some("a")),
];
for (source, expected_int, expected_str) in cases {
let code = compile_exec(source);
assert!(
!code.instructions.iter().any(|unit| matches!(
unit.op,
Instruction::BinaryOp { op }
if op.get(OpArg::new(u32::from(u8::from(unit.arg))))
== oparg::BinaryOperator::Subscr
)),
"expected folded constant subscript for {source:?}, got instructions={:?}",
code.instructions
);
if let Some(expected_int) = expected_int.as_ref() {
let has_small_int = code.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadSmallInt { i }
if BigInt::from(i.get(OpArg::new(u32::from(u8::from(unit.arg)))))
== *expected_int
)
});
let has_const_int = code.constants.iter().any(|constant| {
matches!(constant, ConstantData::Integer { value } if value == expected_int)
});
assert!(
has_small_int || has_const_int,
"missing folded integer constant {expected_int} for {source:?}, instructions={:?}",
code.instructions
);
}
if let Some(expected_str) = expected_str {
assert!(
code.constants.iter().any(|constant| {
matches!(constant, ConstantData::Str { value } if value.to_string() == expected_str)
}),
"missing folded string constant {expected_str:?} for {source:?}",
);
}
}
}
#[test]
fn constant_subscript_registers_source_const_before_result_like_cpython() {
let code = compile_exec("value = 'string'[3]\n");
let source_index = code
.constants
.iter()
.position(|constant| {
matches!(constant, ConstantData::Str { value } if value.to_string() == "string")
})
.expect("missing source string constant");
let result_index = code
.constants
.iter()
.position(|constant| {
matches!(constant, ConstantData::Str { value } if value.to_string() == "i")
})
.expect("missing folded subscript result");
assert!(
source_index < result_index,
"CPython codegen_subscript emits the source constant before flowgraph.c folds NB_SUBSCR"
);
}
#[test]
fn constant_slice_subscript_folds_in_load_context() {
let code = compile_exec(
"\
a = 'hello'[:4]\n\
b = b'abcd'[1:3]\n\
c = (1, 2, 3)[:2]\n",
);
assert!(
!code.instructions.iter().any(|unit| matches!(
unit.op,
Instruction::BinaryOp { op }
if op.get(OpArg::new(u32::from(u8::from(unit.arg))))
== oparg::BinaryOperator::Subscr
)),
"expected folded constant slice subscripts, got instructions={:?}",
code.instructions
);
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Str { value } if value.to_string() == "hell"
)));
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Bytes { value } if value == b"bc"
)));
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if matches!(
elements.as_slice(),
[
ConstantData::Integer { value: a },
ConstantData::Integer { value: b },
] if *a == BigInt::from(1)
&& *b == BigInt::from(2)
)
)));
}
#[test]
fn list_of_constant_tuples_uses_list_extend() {
let code = compile_exec(
"\
deprecated_cases = [('a', 'b'), ('c', 'd'), ('e', 'f'), ('g', 'h'), ('i', 'j')]
",
);
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::ListExtend { .. })),
"expected constant tuple list folding"
);
}
#[test]
fn large_list_of_unary_constants_uses_list_extend() {
let code = compile_exec(
"\
values = [-1, not True, ~0, +True, 5]
",
);
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::ListExtend { .. })),
"expected unary-folded constants to participate in list folding, got instructions={:?}",
code.instructions
);
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if elements.len() == 5
&& matches!(&elements[0], ConstantData::Integer { value } if *value == BigInt::from(-1))
&& matches!(&elements[1], ConstantData::Boolean { value } if !value)
&& matches!(&elements[2], ConstantData::Integer { value } if *value == BigInt::from(-1))
&& matches!(&elements[3], ConstantData::Integer { value } if *value == BigInt::from(1))
&& matches!(&elements[4], ConstantData::Integer { value } if *value == BigInt::from(5))
)));
}
#[test]
fn outer_unary_after_binop_folds_before_list_folding() {
let code = compile_exec(
"\
values = [2.0**53, -0.5, -2.0**-54]
",
);
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::ListExtend { .. })),
"expected binop-folded constants to participate in list folding, got instructions={:?}",
code.instructions
);
assert!(
!code.instructions.iter().any(|unit| matches!(
unit.op,
Instruction::BinaryOp { .. } | Instruction::UnaryNegative
)),
"constant expression list should not leave runtime ops, got instructions={:?}",
code.instructions
);
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if elements.len() == 3
&& matches!(&elements[0], ConstantData::Float { value } if *value == 9007199254740992.0)
&& matches!(&elements[1], ConstantData::Float { value } if *value == -0.5)
&& matches!(&elements[2], ConstantData::Float { value } if value.is_sign_negative())
)));
}
#[test]
fn negative_integer_power_folds_to_float_constant() {
let code = compile_exec("value = -3.0 * 2**(-333)\n");
assert!(
!code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })),
"negative integer power should fold through the enclosing multiply, got instructions={:?}",
code.instructions
);
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Float { value }
if value.is_sign_negative() && *value < 0.0 && value.abs() < 1.0e-90
)));
}
#[test]
fn complex_power_constants_fold_like_cpython() {
let code = compile_exec(
"\
one = 3j ** 0j
zero = 0j ** 2
",
);
assert!(
!code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })),
"safe complex power constants should fold away, got instructions={:?}",
code.instructions
);
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Complex { value } if value.re == 1.0 && value.im == 0.0
)));
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Complex { value } if value.re == 0.0 && value.im == 0.0
)));
}
#[test]
fn folded_nan_constants_are_not_deduplicated_like_cpython() {
let code = compile_exec(
"\
def f():
repr(1e300 * 1e300 * 0)
repr(-1e300 * 1e300 * 0)
str(1e300 * 1e300 * 0)
str(-1e300 * 1e300 * 0)
",
);
let f = find_code(&code, "f").expect("missing function code");
let nan_count = f
.constants
.iter()
.filter(|constant| matches!(constant, ConstantData::Float { value } if value.is_nan()))
.count();
assert_eq!(
nan_count, 4,
"CPython _PyCode_ConstantKey keeps folded NaN constants distinct"
);
}
#[test]
fn zero_complex_power_exception_constants_do_not_fold() {
let code = compile_exec("value = 0j ** (3 - 2j)\n");
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })),
"zero complex to complex power should stay runtime so ZeroDivisionError is preserved, got instructions={:?}",
code.instructions
);
}
#[test]
fn large_constant_list_keeps_streaming_build() {
let source = format!(
"values = [{}]\n",
(0..31)
.map(|i| format!("'v{i}'"))
.collect::<Vec<_>>()
.join(", ")
);
let code = compile_exec(&source);
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::ListAppend { .. })),
"large constant lists should keep LIST_APPEND streaming form, got instructions={:?}",
code.instructions
);
assert!(
!code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::ListExtend { .. })),
"large constant lists should not fold to LIST_EXTEND, got instructions={:?}",
code.instructions
);
}
#[test]
fn large_constant_tuple_stream_folds_to_tuple_const() {
let source = format!(
"values = ({},)\n",
(0..31)
.map(|i| format!("'v{i}'"))
.collect::<Vec<_>>()
.join(", ")
);
let code = compile_exec(&source);
assert!(
!code.instructions.iter().any(|unit| matches!(
unit.op,
Instruction::BuildList { .. }
| Instruction::ListAppend { .. }
| Instruction::CallIntrinsic1 { .. }
)),
"large constant tuple should fold the LIST_TO_TUPLE stream, got instructions={:?}",
code.instructions
);
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements } if elements.len() == 31
)));
}
#[test]
fn annotation_closure_uses_format_varname() {
let code = compile_exec(
"\
class C:
x: int
",
);
let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code");
let varnames = annotate
.varnames
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
assert_eq!(varnames, vec!["format"]);
assert!(annotate.instructions.iter().any(|unit| matches!(
unit.op,
Instruction::LoadFastBorrow { var_num }
if usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg))))) == 0
)));
assert!(!annotate.instructions.iter().any(|unit| matches!(
unit.op,
Instruction::LoadFastCheck { var_num }
if usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg))))) == 0
)));
}
#[test]
fn future_function_signature_annotation_uses_hidden_block_like_cpython() {
let code = compile_exec(
"\
from __future__ import annotations
def f(x: T): pass
",
);
let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code");
let varnames = annotate
.varnames
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
assert_eq!(varnames, vec!["format"]);
assert!(
find_code(&code, "f").is_some(),
"function body symbol-table cursor must skip the hidden AnnotationBlock"
);
}
#[test]
fn future_generic_class_annotations_do_not_capture_type_params_like_cpython() {
let code = compile_exec(
"\
from __future__ import annotations
class A[T, *Ts, **P]:
x: T
y: tuple[*Ts]
z: Callable[P, str]
",
);
let type_params =
find_code(&code, "<generic parameters of A>").expect("missing type parameter scope");
let class = find_direct_child_code(type_params, "A").expect("missing class body");
assert_eq!(
type_params
.cellvars
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>(),
[".type_params"]
);
assert_eq!(
class
.freevars
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>(),
[".type_params"]
);
}
#[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(
"\
format = object()
x: format
",
);
let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code");
let varnames = annotate
.varnames
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
assert_eq!(varnames, vec!["format"]);
assert!(
annotate.names.iter().any(|name| name.as_str() == "format"),
"CPython keeps the helper parameter as internal .format during symbol analysis, so annotation expression `format` must remain a separate name; got names={:?}",
annotate.names
);
let helper_param_loads = annotate
.instructions
.iter()
.filter(|unit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
annotate.varnames[usize::from(var_num.get(arg))].as_str() == "format"
}
_ => false,
})
.count();
assert_eq!(
helper_param_loads, 1,
"only the CPython format-validation prologue should load the helper parameter; annotation expression `format` must not compile as LOAD_FAST"
);
}
#[test]
fn non_simple_class_annotation_is_not_deferred_like_cpython() {
let code = compile_exec(
"\
class C:
x.y: list = []
z: int
",
);
let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code");
let names = annotate
.names
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
assert_eq!(names, vec!["int"]);
}
#[test]
fn non_simple_annotation_only_consumes_symbol_table_cursor() {
let code = compile_exec(
"\
class C:
x.y: (lambda: str) = []
z: (lambda: int)
",
);
let annotate = find_code(&code, "__annotate__").expect("missing __annotate__ code");
let lambdas = annotate
.constants
.iter()
.filter_map(|constant| match constant {
ConstantData::Code { code } if code.obj_name == "<lambda>" => Some(code.as_ref()),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(lambdas.len(), 1);
assert_eq!(
lambdas[0]
.names
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>(),
vec!["int"]
);
}
#[test]
fn class_deferred_annotations_guard_only_conditional_entries_like_cpython() {
let code = compile_exec(
"\
class C:
x: int
if flag:
y: str
z: float
",
);
let class_code = find_code(&code, "C").expect("missing class code");
let class_ops: Vec<_> = class_code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let class_set_adds = class_ops
.iter()
.filter(|op| matches!(op, Instruction::SetAdd { .. }))
.count();
assert_eq!(
class_set_adds, 1,
"CPython _PyCompile_AddDeferredAnnotation() adds class annotations to __conditional_annotations__ only inside conditional blocks, got ops={class_ops:?}"
);
assert!(
class_code.instructions.iter().any(|unit| match unit.op {
Instruction::LoadDeref { i } => {
let idx = i.get(OpArg::new(u32::from(u8::from(unit.arg)))).as_usize();
localsplus_name(class_code, idx) == Some("__conditional_annotations__")
}
_ => false,
}),
"CPython codegen_annassign() emits LOAD_DEREF for class __conditional_annotations__, got ops={class_ops:?}"
);
assert!(
!class_code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFromDictOrDeref { .. })),
"CPython codegen_annassign() bypasses codegen_nameop for class __conditional_annotations__, got ops={class_ops:?}"
);
let annotate = find_code(class_code, "__annotate__").expect("missing __annotate__ code");
let annotate_ops: Vec<_> = annotate
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let annotation_body = annotate_ops
.iter()
.position(|op| matches!(op, Instruction::BuildMap { .. }))
.map(|idx| &annotate_ops[idx..])
.expect("missing annotation map build");
let guarded_entries = annotation_body
.iter()
.filter(|op| matches!(op, Instruction::PopJumpIfFalse { .. }))
.count();
assert_eq!(
guarded_entries, 1,
"CPython codegen_deferred_annotations_body() guards only conditional class annotations, got ops={annotate_ops:?}"
);
}
#[test]
fn future_annotations_non_simple_target_checks_target_but_not_annotation_like_cpython() {
let code = compile_exec(
"\
from __future__ import annotations
class C:
target[item]: missing
",
);
let class_code = find_code(&code, "C").expect("missing class code");
let loaded_names: Vec<_> = class_code
.instructions
.iter()
.filter_map(|unit| match unit.op {
Instruction::LoadName { namei } => {
let idx = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
Some(class_code.names[usize::try_from(idx).unwrap()].as_str())
}
_ => None,
})
.collect();
assert!(
["target", "item"]
.iter()
.all(|name| loaded_names.contains(name)),
"CPython codegen_annassign() still checks bare complex annotation targets under future annotations, got loaded_names={loaded_names:?}"
);
assert!(
!loaded_names.contains(&"missing"),
"CPython codegen_check_annotation() skips the annotation expression under future annotations, got loaded_names={loaded_names:?}"
);
}
#[test]
fn type_param_evaluator_uses_dot_format_varname() {
let code = compile_exec(
"\
class C[T: int]:
pass
",
);
let evaluator = find_code(&code, "T").expect("missing type parameter evaluator");
let varnames = evaluator
.varnames
.iter()
.map(|name| name.as_str())
.collect::<Vec<_>>();
assert_eq!(varnames, vec![".format"]);
}
#[test]
fn generic_class_double_star_bases_use_tuple_ex_call_path() {
let code = compile_exec(
"\
def f(Base, kwargs):
class C[T](Base, **kwargs):
pass
return C
",
);
let type_params =
find_code(&code, "<generic parameters of C>").expect("missing type params code");
let ops: Vec<_> = type_params
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!has_intrinsic_1(type_params, IntrinsicFunction1::ListToTuple),
"generic class call with **kwargs but no starred bases should not use list-to-tuple, got ops={ops:?}"
);
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::BuildTuple { .. },
Instruction::BuildMap { .. },
Instruction::LoadDeref { .. } | Instruction::LoadFast { .. },
Instruction::DictMerge { .. },
]
)
}),
"expected CPython-style BUILD_TUPLE/BUILD_MAP/DICT_MERGE ex-call path, got ops={ops:?}"
);
}
#[test]
fn generic_function_defaults_call_type_params_like_cpython() {
let code = compile_exec(
"\
def func[T](a: T = 'a', *, b: T = 'b'):
return a, b
",
);
let ops: Vec<_> = code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::Swap { .. },
Instruction::LoadConst { .. },
Instruction::MakeFunction,
Instruction::Swap { .. },
Instruction::Call { .. },
]
)
}),
"expected CPython generic defaults call pattern SWAP/MAKE_FUNCTION/SWAP/CALL, got ops={ops:?}"
);
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::MakeFunction,
Instruction::Swap { .. },
Instruction::Swap { .. },
Instruction::PushNull,
Instruction::Swap { .. },
]
)
}),
"CPython generic defaults use SWAP/CALL after codegen_make_closure(), not a PUSH_NULL reshuffle, got ops={ops:?}"
);
}
#[test]
fn generic_function_type_params_reserve_defaults_like_cpython() {
let code = compile_exec(
"\
def func[T]():
pass
",
);
let type_params =
find_code(&code, "<generic parameters of func>").expect("missing type params code");
// The slot is reserved even though nothing is passed into it.
assert_eq!(type_params.arg_count, 0);
assert_eq!(
type_params
.varnames
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec![".defaults", "T"]
);
}
#[test]
fn generic_function_type_params_split_defaults_like_cpython() {
let code = compile_exec(
"\
def with_pos[T](a: T = 1):
pass
def with_kw[U](*, a: U = 1):
pass
",
);
let with_pos =
find_code(&code, "<generic parameters of with_pos>").expect("missing type params code");
let with_kw =
find_code(&code, "<generic parameters of with_kw>").expect("missing type params code");
assert_eq!(
with_pos
.varnames
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec![".defaults"]
);
assert_eq!(with_pos.arg_count, 1);
assert_eq!(
with_kw
.varnames
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec![".defaults", ".kwdefaults"]
);
assert_eq!(with_kw.arg_count, 1);
}
#[test]
fn class_type_param_bound_prefers_classdict_over_outer_function_local() {
let code = compile_exec(
"\
def f(self):
class X:
T = int
def foo[U: T](self): ...
T, U = X.foo.__type_params__
return T.__bound__, U.__bound__
",
);
let f = find_code(&code, "f").expect("missing f code");
assert!(
!f.cellvars.iter().any(|name| name == "T"),
"class-local type-param bound must not force outer function T cell, got cellvars={:?}",
f.cellvars
);
let class_code = find_code(f, "X").expect("missing X class code");
assert!(
!class_code.freevars.iter().any(|name| name == "T"),
"class body must not close over outer T for class-local bound, got freevars={:?}",
class_code.freevars
);
let type_params =
find_code(class_code, "<generic parameters of foo>").expect("missing type params code");
assert_eq!(
type_params
.freevars
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec!["__classdict__"],
"type params scope should close only over __classdict__, got freevars={:?}",
type_params.freevars
);
let bound = find_code(type_params, "U").expect("missing U bound code");
assert_eq!(
bound
.freevars
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec!["__classdict__"],
"bound evaluator should close only over __classdict__, got freevars={:?}",
bound.freevars
);
assert!(
bound
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFromDictOrGlobals { .. })),
"bound evaluator should use LOAD_FROM_DICT_OR_GLOBALS for class-local T, got instructions={:?}",
bound.instructions
);
}
#[test]
fn class_type_param_bound_respects_class_global_over_outer_function_local() {
let code = compile_exec(
"\
def f(self):
T = int
class X:
global T
def foo[U: T](self): ...
return X.foo.__type_params__
",
);
let f = find_code(&code, "f").expect("missing f code");
assert!(
f.cellvars.is_empty(),
"class global type-param bound must not force outer function cells, got cellvars={:?}",
f.cellvars
);
let class_code = find_code(f, "X").expect("missing X class code");
let type_params =
find_code(class_code, "<generic parameters of foo>").expect("missing type params code");
let bound = find_code(type_params, "U").expect("missing U bound code");
assert!(
bound
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadGlobal { .. })),
"explicit class global should resolve as LOAD_GLOBAL in bound evaluator, got instructions={:?}",
bound.instructions
);
assert!(
!bound.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFromDictOrGlobals { .. }
| Instruction::LoadFromDictOrDeref { .. }
)
}),
"explicit class global should not use classdict/deref lookup, got instructions={:?}",
bound.instructions
);
}
#[test]
fn generic_type_alias_in_class_does_not_capture_module_name() {
let code = compile_exec(
r#"
T = U = "global"
class C:
T = "class"
U = "class"
type Alias[T] = lambda: (T, U)
"#,
);
assert!(
code.cellvars.is_empty(),
"module T must remain a global name, got cellvars={:?}",
code.cellvars
);
let class_code = find_code(&code, "C").expect("missing class code");
assert!(
class_code.freevars.is_empty(),
"plain module-level class must not close over module T, got freevars={:?}",
class_code.freevars
);
let type_params = find_code(class_code, "<generic parameters of Alias>")
.expect("missing alias type params");
assert_eq!(
type_params
.cellvars
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec!["T"],
"alias type parameter T should be local to the type-params scope, got cellvars={:?}",
type_params.cellvars
);
assert_eq!(
type_params
.freevars
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec!["__classdict__"],
"alias type params should close only over the classdict, got freevars={:?}",
type_params.freevars
);
let alias = find_code(type_params, "Alias").expect("missing alias value code");
assert_eq!(
alias
.freevars
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec!["T", "__classdict__"],
"alias value should close over its type parameter and classdict, got freevars={:?}",
alias.freevars
);
let lambda = find_code(alias, "<lambda>").expect("missing alias lambda");
assert_eq!(
lambda
.freevars
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec!["T"],
"lambda should close over alias type parameter T only, got freevars={:?}",
lambda.freevars
);
}
#[test]
fn nested_generic_class_base_child_free_keeps_classdict_lookup() {
for (child_name, base_expr) in [
("<genexpr>", "make_base(T for _ in (1,))"),
("<listcomp>", "make_base([T for _ in (1,)])"),
("<lambda>", "make_base(lambda: T)"),
] {
let code = compile_exec(&format!(
"\
class C[T]:
T = 'class'
class Inner[U]({base_expr}, make_base(T)):
pass
"
));
let type_params = find_code(&code, "<generic parameters of Inner>")
.expect("missing inner type params code");
assert_eq!(
type_params
.freevars
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec!["T", "__classdict__"],
"inner type params should keep T as a child-only freevar for {child_name}, got freevars={:?}",
type_params.freevars
);
assert!(
type_params
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFromDictOrGlobals { .. })),
"direct class-local T lookup should still use classdict/global path for {child_name}, got instructions={:?}",
type_params.instructions
);
let child = find_code(type_params, child_name).expect("missing child code");
assert_eq!(
child
.freevars
.iter()
.map(String::as_str)
.collect::<Vec<_>>(),
vec!["T"],
"{child_name} should close over T from the type-params scope, got freevars={:?}",
child.freevars
);
}
}
#[test]
fn non_inlined_listcomp_return_uses_comprehension_location_like_cpython() {
let code = compile_exec(
"\
class C[T]:
class Inner[U](
make_base([T for _ in (1,)])
):
pass
",
);
let listcomp = find_code(&code, "<listcomp>").expect("missing listcomp code");
let return_positions: Vec<_> = listcomp
.instructions
.iter()
.zip(&listcomp.locations)
.filter_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::ReturnValue).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.collect();
assert_eq!(
return_positions,
vec![(3, 19, 3, 36)],
"CPython codegen_comprehension() emits non-gen RETURN_VALUE at LOC(e)"
);
}
#[test]
fn class_annotation_global_resolution_matches_cpython() {
let class_global = compile_exec(
"\
X = 'global'
class C:
locals()['X'] = 'class'
global X
y: X
",
);
let annotate =
find_code(&class_global, "__annotate__").expect("missing class __annotate__ code");
assert!(
annotate
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadGlobal { .. })),
"expected explicit class global to use LOAD_GLOBAL, got instructions={:?}",
annotate.instructions
);
assert!(
!annotate
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFromDictOrGlobals { .. })),
"did not expect class explicit global to use LOAD_FROM_DICT_OR_GLOBALS, got instructions={:?}",
annotate.instructions
);
let outer_global = compile_exec(
"\
def f():
global X
class C:
locals()['X'] = 'class'
y: X
",
);
let annotate = find_code(&outer_global, "__annotate__")
.expect("missing nested class __annotate__ code");
assert!(
annotate
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFromDictOrGlobals { .. })),
"expected outer explicit global in class annotation to use LOAD_FROM_DICT_OR_GLOBALS, got instructions={:?}",
annotate.instructions
);
}
#[test]
fn constant_tuple_binops_fold_like_cpython() {
let code = compile_exec("value = (1,) * 17 + ('spam',)\n");
assert!(
!code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BinaryOp { .. })),
"tuple constant binops should fold away, got instructions={:?}",
code.instructions
);
assert!(code.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if elements.len() == 18
&& elements[..17]
.iter()
.all(|elt| matches!(elt, ConstantData::Integer { value } if *value == BigInt::from(1)))
&& matches!(&elements[17], ConstantData::Str { value } if value.to_string() == "spam")
)));
}
#[test]
fn tuple_not_keeps_to_bool_unary_not_like_cpython() {
let code = compile_exec(
"\
def f():
return not ()
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
assert!(
ops.windows(3).any(|window| {
matches!(window[0].op, Instruction::LoadConst { consti }
if matches!(
&f.constants[consti.get(OpArg::new(u32::from(u8::from(window[0].arg))))],
ConstantData::Tuple { elements } if elements.is_empty()
)) && matches!(window[1].op, Instruction::ToBool)
&& matches!(window[2].op, Instruction::UnaryNot)
}),
"CPython codegen emits TO_BOOL; UNARY_NOT for UnaryOp(Not), while flowgraph.c folds tuple literals only after the LOAD_CONST+TO_BOOL pass, got instructions={:?}",
f.instructions
);
}
#[test]
fn tuple_if_test_keeps_to_bool_jump_like_cpython() {
let code = compile_exec(
"\
def f():
if ():
return 1
return 2
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect::<Vec<_>>();
assert!(
ops.windows(3).any(|window| {
matches!(window[0].op, Instruction::LoadConst { consti }
if matches!(
&f.constants[consti.get(OpArg::new(u32::from(u8::from(window[0].arg))))],
ConstantData::Tuple { elements } if elements.is_empty()
)) && matches!(window[1].op, Instruction::ToBool)
&& matches!(window[2].op, Instruction::PopJumpIfFalse { .. })
}),
"CPython leaves tuple literal truth tests as LOAD_CONST tuple; TO_BOOL; POP_JUMP_IF_FALSE because tuple folding happens after constant jump folding, got instructions={:?}",
f.instructions
);
}
#[test]
fn constant_list_iterable_uses_tuple() {
let code = compile_exec(
"\
def f():
return {x: y for x, y in [(1, 2), ]}
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
!f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BuildList { .. })),
"constant list iterable should avoid BUILD_LIST before GET_ITER"
);
assert!(f.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if matches!(
elements.as_slice(),
[ConstantData::Tuple { elements: inner }]
if matches!(
inner.as_slice(),
[
ConstantData::Integer { .. },
ConstantData::Integer { .. }
]
)
)
)));
}
#[test]
fn constant_list_iterable_preserves_cpython_const_order() {
let code = compile_exec(
"\
def f():
for x in ['a', 'b', 'c']:
pass
",
);
let f = find_code(&code, "f").expect("missing function code");
let constants = f.constants.iter().collect::<Vec<_>>();
assert!(
matches!(constants[0], ConstantData::Str { value } if value.to_string() == "a"),
"CPython emits list elements as LOAD_CONST before flowgraph folds GET_ITER lists"
);
assert!(matches!(constants[1], ConstantData::None));
assert!(matches!(
constants[2],
ConstantData::Tuple { elements }
if matches!(
elements.as_slice(),
[
ConstantData::Str { value: first },
ConstantData::Str { value: second },
ConstantData::Str { value: third },
] if first.to_string() == "a"
&& second.to_string() == "b"
&& third.to_string() == "c"
)
));
}
#[test]
fn try_except_folded_tuple_consts_follow_cpython_block_order() {
let code = compile_exec(
"\
def f(macrelease):
try:
g()
except ValueError:
macrelease = (10, 3)
if macrelease >= (10, 4):
pass
",
);
let f = find_code(&code, "f").expect("missing function code");
let constants = f.constants.iter().collect::<Vec<_>>();
assert!(
constants.windows(2).any(|window| {
matches!(
window,
[
ConstantData::Tuple { elements: first },
ConstantData::Tuple { elements: second },
] if matches!(
(first.as_slice(), second.as_slice()),
(
[
ConstantData::Integer { value: a },
ConstantData::Integer { value: b },
],
[
ConstantData::Integer { value: c },
ConstantData::Integer { value: d },
],
) if a == &BigInt::from(10)
&& b == &BigInt::from(3)
&& c == &BigInt::from(10)
&& d == &BigInt::from(4)
)
)
}),
"CPython flowgraph.c walks b_next order, so the except-body tuple is folded before the following if-test tuple; got {constants:?}"
);
}
#[test]
fn small_set_membership_folds_before_later_unary_const_like_cpython() {
let code = compile_exec(
r#"
def f(method, n):
if method not in {"linear", "ranked"}:
pass
if method == "ranked":
start = (n - 1) / -2
"#,
);
let f = find_code(&code, "f").expect("missing function code");
let constants = f.constants.iter().collect::<Vec<_>>();
let frozenset_index = constants
.iter()
.position(|constant| matches!(constant, ConstantData::Frozenset { .. }))
.expect("missing folded membership frozenset");
let negative_two_index = constants
.iter()
.position(|constant| {
matches!(
constant,
ConstantData::Integer { value } if value == &BigInt::from(-2)
)
})
.expect("missing folded -2 constant");
assert!(
frozenset_index < negative_two_index,
"CPython flowgraph.c optimizes BUILD_SET+CONTAINS_OP inline before folding the later unary -2; got {constants:?}"
);
}
#[test]
fn boolop_const_order_keeps_cpython_codegen_constants() {
let code = compile_exec(
"\
def or_false(x):
return False or x
def zero_or_tuple():
return 0 or (1, -1)
def tuple_or_tuple():
return (1, -1) or (-1, 1)
",
);
let or_false = find_code(&code, "or_false").expect("missing or_false code");
let constants = or_false.constants.iter().collect::<Vec<_>>();
assert_eq!(constants.len(), 1);
assert!(
matches!(constants[0], ConstantData::Boolean { value: false }),
"CPython registers the skipped boolop literal before flowgraph removes the branch"
);
let zero_or_tuple = find_code(&code, "zero_or_tuple").expect("missing zero_or_tuple code");
let constants = zero_or_tuple.constants.iter().collect::<Vec<_>>();
assert_eq!(constants.len(), 2);
assert!(
matches!(
constants[0],
ConstantData::Integer { value } if value == &BigInt::from(0)
) && matches!(
constants[1],
ConstantData::Tuple { elements }
if matches!(
elements.as_slice(),
[
ConstantData::Integer { value: one },
ConstantData::Integer { value: minus_one },
] if one == &BigInt::from(1) && minus_one == &BigInt::from(-1)
)
),
"CPython keeps the skipped scalar literal before the folded tuple constant"
);
let tuple_or_tuple =
find_code(&code, "tuple_or_tuple").expect("missing tuple_or_tuple code");
let constants = tuple_or_tuple.constants.iter().collect::<Vec<_>>();
assert_eq!(constants.len(), 3);
assert!(
matches!(
constants[0],
ConstantData::Integer { value } if value == &BigInt::from(1)
) && matches!(
constants[1],
ConstantData::Tuple { elements }
if matches!(
elements.as_slice(),
[
ConstantData::Integer { value: one },
ConstantData::Integer { value: minus_one },
] if one == &BigInt::from(1) && minus_one == &BigInt::from(-1)
)
) && matches!(
constants[2],
ConstantData::Tuple { elements }
if matches!(
elements.as_slice(),
[
ConstantData::Integer { value: minus_one },
ConstantData::Integer { value: one },
] if minus_one == &BigInt::from(-1) && one == &BigInt::from(1)
)
),
"CPython compiles boolop tuple heads before flowgraph folds them"
);
}
#[test]
fn chained_compare_jump_if_runs_cpython_check_compare_warning() {
let message = first_exec_warning(
"\
def f(x):
if 1 is 1 < x:
return x
",
);
assert!(
message.contains("\"is\" with 'int' literal"),
"CPython codegen_jump_if() checks chained comparisons before conditional lowering, got {message:?}"
);
}
#[test]
fn lambda_without_body_constants_keeps_none_like_cpython() {
let code = compile_exec("f = lambda x: x");
let lambda = find_code(&code, "<lambda>").expect("missing lambda code");
let constants = lambda.constants.iter().collect::<Vec<_>>();
assert_eq!(constants.len(), 1);
assert!(
matches!(constants[0], ConstantData::None),
"CPython AddReturnAtEnd registers None for constant-free lambdas"
);
}
#[test]
fn generator_lambda_without_body_constants_omits_none_like_cpython() {
let code = compile_exec("f = lambda x: (yield x)");
let lambda = find_code(&code, "<lambda>").expect("missing lambda code");
assert!(
lambda.constants.is_empty(),
"CPython codegen_lambda() assembles generator lambdas with addNone=0"
);
}
#[test]
fn call_function_ex_empty_args_tuple_is_folded_late_like_cpython() {
let code = compile_exec(
"\
def f(g, kwargs, ns):
g(**kwargs)
ns['T']
",
);
let f = find_code(&code, "f").expect("missing function code");
let constants = f.constants.iter().collect::<Vec<_>>();
assert_eq!(constants.len(), 3);
assert!(
matches!(constants[0], ConstantData::Str { value } if value.to_string() == "T")
&& matches!(constants[1], ConstantData::None)
&& matches!(constants[2], ConstantData::Tuple { elements } if elements.is_empty()),
"CPython emits BUILD_TUPLE 0 for CALL_FUNCTION_EX args and folds it after earlier constants"
);
}
#[test]
fn large_constant_list_iterable_keeps_streaming_list_build() {
let source = format!(
"def f():\n for x in [{}]:\n pass\n",
(0..=STACK_USE_GUIDELINE)
.map(|i| format!("'v{i}'"))
.collect::<Vec<_>>()
.join(", ")
);
let code = compile_exec(&source);
let f = find_code(&code, "f").expect("missing function code");
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BuildList { .. })),
"large list iterable should keep CPython streaming BUILD_LIST form, got instructions={:?}",
f.instructions
);
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::ListAppend { .. })),
"large list iterable should use LIST_APPEND streaming form, got instructions={:?}",
f.instructions
);
}
#[test]
fn constant_set_iterable_uses_frozenset_const() {
let code = compile_exec(
"\
def f():
return [x for x in {1, 2, 3}]
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
!f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BuildSet { .. })),
"constant set iterable should avoid BUILD_SET before GET_ITER"
);
assert!(f.constants.iter().any(|constant| matches!(
constant,
ConstantData::Frozenset { elements }
if matches!(
elements.as_slice(),
[
ConstantData::Integer { .. },
ConstantData::Integer { .. },
ConstantData::Integer { .. }
]
)
)));
}
#[test]
fn constant_list_membership_uses_tuple_const() {
let code = compile_exec(
"\
f = lambda x: x in [1, 2, 3]
",
);
let lambda = find_code(&code, "<lambda>").expect("missing lambda code");
assert!(
!lambda
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BuildList { .. })),
"constant list membership should avoid BUILD_LIST before CONTAINS_OP"
);
assert!(lambda.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if matches!(
elements.as_slice(),
[
ConstantData::Integer { .. },
ConstantData::Integer { .. },
ConstantData::Integer { .. }
]
)
)));
}
#[test]
fn small_constant_set_membership_uses_frozenset_const() {
let code = compile_exec(
"\
f = lambda x: x in {0}
",
);
let lambda = find_code(&code, "<lambda>").expect("missing lambda code");
assert!(
!lambda
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BuildSet { .. })),
"constant set membership should avoid BUILD_SET before CONTAINS_OP"
);
assert!(lambda.constants.iter().any(|constant| matches!(
constant,
ConstantData::Frozenset { elements }
if matches!(elements.as_slice(), [ConstantData::Integer { value }] if *value == BigInt::from(0))
)));
}
#[test]
fn frozenset_membership_consts_deduplicate_like_cpython_constant_key() {
let code = compile_exec(
"\
def f(x):
return x in {1, 2}, x in {2, 1}, x in {1, 1}
",
);
let f = find_code(&code, "f").expect("missing function code");
let frozensets: Vec<_> = f
.constants
.iter()
.filter_map(|constant| match constant {
ConstantData::Frozenset { elements } => Some(elements.as_slice()),
_ => None,
})
.collect();
assert_eq!(
frozensets.len(),
2,
"CPython folds equal frozensets to the same const key and removes duplicate set items"
);
assert!(
frozensets.iter().any(|elements| elements.len() == 2),
"missing shared frozenset constant for {{1, 2}} and {{2, 1}}"
);
assert!(
frozensets.iter().any(|elements| elements.len() == 1),
"missing duplicate-collapsed frozenset constant for {{1, 1}}"
);
}
#[test]
fn nonconstant_list_membership_uses_tuple() {
let code = compile_exec(
"\
def f(a, b, c, x):
return x in [a, b, c]
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(2).any(|window| {
matches!(
window,
[
Instruction::BuildTuple { .. },
Instruction::ContainsOp { .. }
]
)
}),
"expected BUILD_TUPLE before CONTAINS_OP for non-constant list membership, got ops={ops:?}"
);
}
#[test]
fn unary_not_membership_and_identity_invert_compare_op() {
let code = compile_exec(
"\
def f(a, b, d):
x = not (a in d)
y = not (a is b)
return x, y
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect();
assert!(
!ops.iter().any(|op| matches!(op, Instruction::UnaryNot)),
"CPython folds CONTAINS_OP/IS_OP + UNARY_NOT into inverted op, got ops={ops:?}"
);
assert!(instructions.iter().any(|unit| {
matches!(unit.op, Instruction::ContainsOp { invert } if invert.get(OpArg::new(unit.arg.as_u32())) == Invert::Yes)
}));
assert!(instructions.iter().any(|unit| {
matches!(unit.op, Instruction::IsOp { invert } if invert.get(OpArg::new(unit.arg.as_u32())) == Invert::Yes)
}));
}
#[test]
fn starred_tuple_iterable_drops_list_to_tuple_before_get_iter() {
let code = compile_exec(
"\
def f(a, b, c):
for x in *a, *b, *c:
pass
",
);
let f = find_code(&code, "f").expect("missing function code");
assert!(
!has_intrinsic_1(f, IntrinsicFunction1::ListToTuple),
"LIST_TO_TUPLE should be removed before GET_ITER in for-iterable context"
);
assert!(
f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::GetIter)),
"expected GET_ITER in for loop"
);
}
#[test]
fn comprehension_single_list_iterable_uses_tuple() {
let code = compile_exec(
"\
def g():
[x for x in [(yield 1)]]
",
);
let g = find_code(&code, "g").expect("missing g code");
let ops: Vec<_> = g
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(2).any(|window| {
matches!(
window,
[Instruction::BuildTuple { .. }, Instruction::GetIter]
)
}),
"expected BUILD_TUPLE before GET_ITER for single-item list iterable in comprehension, got ops={ops:?}"
);
}
#[test]
fn comprehension_list_iterable_build_uses_iter_location_like_cpython() {
let code = compile_exec(
"\
async def f(i):
return i
async def run_list():
return [await c for c in [f(1), f(41)]]
",
);
let run_list = find_code(&code, "run_list").expect("missing run_list code");
assert_eq!(
run_list.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xdc, 0x1e, 0x1f, 0xa0, 0x01, 0x9b, 0x64, 0xa4, 0x41, 0xa0,
0x62, 0xa3, 0x45, 0x99, 0x5d, 0xd3, 0x0b, 0x2b, 0x99, 0x5d, 0x98, 0x01, 0x8f, 0x47,
0x8a, 0x47, 0x99, 0x5d, 0xd1, 0x0b, 0x2b, 0xd0, 0x04, 0x2b, 0x89, 0x47, 0xf9, 0xd2,
0x0b, 0x2b, 0xf9,
],
"CPython codegen_comprehension_iter() emits GET_ITER at LOC(comp->iter)"
);
}
#[test]
fn comprehension_boolop_iter_get_iter_uses_iter_location_like_cpython() {
let code = compile_exec(
"\
def f(self):
return any(not w.cancelled() for w in (self._waiters or ()))
",
);
let f = find_code(&code, "f").expect("missing f code");
let get_iter_positions: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::GetIter).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.collect();
assert!(
get_iter_positions.contains(&(2, 44, 2, 63)),
"CPython codegen_comprehension_iter() emits GET_ITER at LOC(comp->iter), got {get_iter_positions:?}"
);
}
#[test]
fn inlined_comprehension_backedges_use_element_location_like_cpython() {
let code = compile_exec(
"\
async def f(i):
return i
async def run_list():
return [s for c in [f(''), f('abc')] for s in await c]
",
);
let run_list = find_code(&code, "run_list").expect("missing run_list code");
assert_eq!(
run_list.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xdc, 0x18, 0x19, 0x98, 0x22, 0x9b, 0x05, 0x9c, 0x71, 0xa0,
0x15, 0x9b, 0x78, 0xd1, 0x17, 0x28, 0xd4, 0x0b, 0x3a, 0xd1, 0x17, 0x28, 0x90, 0x21,
0xb7, 0x27, 0xb2, 0x27, 0xa8, 0x51, 0x8a, 0x41, 0xb1, 0x27, 0x89, 0x41, 0xd1, 0x17,
0x28, 0xd2, 0x0b, 0x3a, 0xd0, 0x04, 0x3a, 0xb1, 0x27, 0xf9, 0xd3, 0x0b, 0x3a, 0xf9,
],
"CPython codegen_sync_comprehension_generator() emits comprehension backedges at elt_loc"
);
}
#[test]
fn nested_dict_comprehension_outer_backedge_uses_key_location_like_cpython() {
let code = compile_exec(
"\
def f(items):
return {op: i for i, ops in items for op in ops}
",
);
let f = find_code(&code, "f").expect("missing function code");
let backedge_positions: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::JumpBackward { .. }).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.collect();
assert!(
backedge_positions.contains(&(2, 13, 2, 18)),
"CPython extends only the terminal dict-comprehension MAP_ADD/backedge location from key through value, got {backedge_positions:?}"
);
assert!(
backedge_positions.contains(&(2, 13, 2, 15)),
"CPython keeps outer dict-comprehension generator backedges at LOC(key), got {backedge_positions:?}"
);
}
#[test]
fn inlined_comprehension_filter_jump_uses_element_location_like_cpython() {
let code = compile_exec(
"\
def f(self):
return [action for action in self._actions if action.option_strings]
",
);
let f = find_code(&code, "f").expect("missing function code");
let filter_jump_position = f
.instructions
.iter()
.zip(&f.locations)
.find_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::PopJumpIfTrue { .. }).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.expect("missing optimized filter jump");
assert_eq!(
filter_jump_position,
(2, 13, 2, 19),
"CPython inlined comprehension filter jump inherits the element/backedge location after CFG cleanup"
);
}
#[test]
fn inlined_comprehension_ifexp_guard_jump_uses_body_location_like_cpython() {
let code = compile_exec(
"\
def f(fields):
return [f for f in fields if (f.compare if f.hash is None else f.hash)]
",
);
let f = find_code(&code, "f").expect("missing function code");
let jump_forward_position = f
.instructions
.iter()
.zip(&f.locations)
.find_map(|(unit, (location, end_location))| {
matches!(unit.op, Instruction::JumpForward { .. }).then_some((
location.line.get(),
location.character_offset.get(),
end_location.line.get(),
end_location.character_offset.get(),
))
})
.expect("missing if-expression body jump");
assert_eq!(
jump_forward_position,
(2, 35, 2, 44),
"CPython flowgraph.c::propagate_line_numbers() copies the if-expression body location onto the NO_LOCATION jump"
);
}
#[test]
fn inlined_async_comprehension_end_async_for_uses_comprehension_location_like_cpython() {
let code = compile_exec(
"\
async def f(it):
for i in it:
yield i
async def run_list():
return [i + 1 async for i in f([10, 20])]
",
);
let run_list = find_code(&code, "run_list").expect("missing run_list code");
assert_eq!(
run_list.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xdc, 0x21, 0x22, 0xa0, 0x42, 0xa8, 0x02, 0xa0, 0x38, 0xa4,
0x1b, 0xd7, 0x0b, 0x2d, 0xd3, 0x0b, 0x2d, 0x98, 0x41, 0x90, 0x01, 0x8f, 0x45, 0x88,
0x45, 0xd4, 0x0b, 0x2d, 0xd0, 0x04, 0x2d, 0xf9, 0xd2, 0x0b, 0x2d, 0xf9,
],
"CPython codegen_async_comprehension_generator() emits END_ASYNC_FOR at comprehension loc"
);
}
#[test]
fn async_for_anext_sequence_uses_statement_location_like_cpython() {
let code = compile_exec(
"\
async def f(source, buffer):
async for i1, i2 in source():
buffer.append(i1 + i2)
",
);
let f = find_code(&code, "f").expect("missing f code");
assert_eq!(
f.linetable.as_ref(),
&[
0xe9, 0x00, 0x80, 0x00, 0xd9, 0x18, 0x1e, 0x9c, 0x08, 0xf7, 0x00, 0x01, 0x05, 0x1f,
0xf0, 0x00, 0x01, 0x05, 0x1f, 0x89, 0x66, 0x88, 0x62, 0xd8, 0x08, 0x0e, 0x8f, 0x0d,
0x89, 0x0d, 0x90, 0x62, 0x95, 0x67, 0xd6, 0x08, 0x1e, 0xf1, 0x03, 0x01, 0x05, 0x1f,
0x9a, 0x08, 0xf9,
],
"CPython codegen_async_for() emits GET_ANEXT/yield-from scaffolding at LOC(s)"
);
}
#[test]
fn nested_comprehension_list_iterable_uses_tuple() {
let code = compile_exec(
"\
def f():
return [[y for y in [x, x + 1]] for x in [1, 3, 5]]
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(2).any(|window| {
matches!(
window,
[Instruction::BuildTuple { .. }, Instruction::GetIter]
)
}),
"expected BUILD_TUPLE before GET_ITER for nested list iterable in comprehension, got ops={ops:?}"
);
}
#[test]
fn comprehension_singleton_sub_iter_uses_assignment_idiom() {
let code = compile_exec(
"\
def f():
return {j: j * j for i in range(4) for j in [i + 1]}
",
);
let f = find_code(&code, "f").expect("missing f code");
let for_iter_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::ForIter { .. }))
.count();
let has_map_add_depth_2 = f.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::MapAdd { i }
if i.get(OpArg::new(u32::from(u8::from(unit.arg)))) == 2
)
});
assert_eq!(
for_iter_count, 1,
"singleton sub-iter should not emit its own FOR_ITER, got instructions={:?}",
f.instructions
);
assert!(
has_map_add_depth_2,
"assignment-idiom dictcomp should use MAP_ADD depth 2, got instructions={:?}",
f.instructions
);
assert!(
!f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::BuildTuple { .. })),
"singleton sub-iter should not materialize an iterator tuple, got instructions={:?}",
f.instructions
);
}
#[test]
fn constant_comprehension_iterable_with_unary_int_uses_tuple_const() {
let code = compile_exec(
"\
l = lambda : [2 < x for x in [-1, 3, 0]]
",
);
let lambda = find_code(&code, "<lambda>").expect("missing lambda code");
assert!(
lambda.constants.iter().any(|constant| matches!(
constant,
ConstantData::Tuple { elements }
if matches!(
elements.as_slice(),
[
ConstantData::Integer { .. },
ConstantData::Integer { .. },
ConstantData::Integer { .. }
]
)
)),
"expected folded tuple constant for comprehension iterable"
);
}
#[test]
fn module_scope_listcomp_is_inlined() {
let code = compile_exec("values = [i for i in range(3)]\n");
assert!(
find_code(&code, "<listcomp>").is_none(),
"module-scope list comprehension should be inlined"
);
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastAndClear { .. })),
"inlined module-scope list comprehension should use LOAD_FAST_AND_CLEAR, got instructions={:?}",
code.instructions
);
}
#[test]
fn module_scope_dictcomp_is_inlined() {
let code = compile_exec("mapping = {i: i for i in range(3)}\n");
assert!(
find_code(&code, "<dictcomp>").is_none(),
"module-scope dict comprehension should be inlined"
);
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastAndClear { .. })),
"inlined module-scope dict comprehension should use LOAD_FAST_AND_CLEAR, got instructions={:?}",
code.instructions
);
}
#[test]
fn async_dictcomp_in_async_function_is_inlined() {
let code = compile_exec(
"\
async def f(items):
return {item: item async for item in items}
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
find_code(&code, "<dictcomp>").is_none(),
"async dict comprehension should be inlined"
);
assert!(
ops.iter().any(|op| matches!(op, Instruction::GetAiter)),
"inlined async dict comprehension should keep GET_AITER in outer code, got ops={ops:?}"
);
assert!(
ops.iter()
.any(|op| matches!(op, Instruction::LoadFastAndClear { .. })),
"inlined async dict comprehension should use LOAD_FAST_AND_CLEAR, got ops={ops:?}"
);
assert!(
!ops.iter().any(|op| matches!(op, Instruction::MakeFunction)),
"inlined async dict comprehension should not materialize MAKE_FUNCTION, got ops={ops:?}"
);
}
#[test]
fn async_inlined_comprehension_inlines_restore_return_into_end_async_for() {
let code = compile_exec(
"\
async def f():
return [i + 1 async for i in g([10, 20])]
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(8).any(|window| {
matches!(
window,
[
Instruction::EndAsyncFor,
Instruction::Swap { .. },
Instruction::StoreFast { .. },
Instruction::ReturnValue,
Instruction::Swap { .. },
Instruction::PopTop,
Instruction::Swap { .. },
Instruction::StoreFast { .. },
]
)
}),
"expected CPython-style restore+return inlined into END_ASYNC_FOR before cleanup, got ops={ops:?}"
);
assert!(
!ops.windows(2).any(|window| {
matches!(
window,
[
Instruction::EndAsyncFor,
Instruction::JumpForward { .. } | Instruction::JumpBackward { .. },
]
)
}),
"unexpected jump from END_ASYNC_FOR to the normal restore tail, got ops={ops:?}"
);
}
#[test]
fn await_cleanup_throw_falls_through_until_cold_reorder() {
let code = compile_exec(
"\
async def f():
await 1
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::CleanupThrow,
Instruction::JumpBackwardNoInterrupt { .. },
Instruction::CallIntrinsic1 { .. },
]
)
}),
"expected CPython-style cold CLEANUP_THROW jump before StopIteration handler, got ops={ops:?}"
);
assert!(
!ops.windows(2).any(|window| {
matches!(window, [Instruction::CleanupThrow, Instruction::EndSend])
}),
"CLEANUP_THROW should not inline the normal END_SEND return tail, got ops={ops:?}"
);
}
#[test]
fn match_async_inlined_comprehension_success_jump_layout() {
let code = compile_exec(
"\
async def f(name_3, name_5):
match b'':
case True:
pass
case name_5 if f'e':
{name_3: f async for name_2 in name_5}
case []:
pass
[[]]
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::JumpForward { .. },
Instruction::Copy { .. },
Instruction::StoreFast { .. },
]
)
}),
"expected CPython-style plain match success jump before async comprehension case, got ops={ops:?}"
);
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::StoreFast { .. },
Instruction::JumpBackwardNoInterrupt { .. },
Instruction::CallIntrinsic1 { .. },
]
)
}),
"CPython codegen_pop_inlined_comprehension_locals() emits JUMP_NO_INTERRUPT before the cleanup path; after flowgraph reordering it remains a backward no-interrupt jump before the StopIteration handler, got ops={ops:?}"
);
}
#[test]
fn for_loop_if_return_reorders_continue_backedge_before_exit_body() {
let code = compile_exec(
"\
def f(items, occurrence):
for item in items:
if item:
occurrence -= 1
if not occurrence:
return item
return None
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. },
]
)
}),
"expected CPython-style inverted return guard followed by loop backedge, got ops={ops:?}"
);
assert!(
!ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
]
)
}),
"return guard should not fall through into the return body before the loop backedge, got ops={ops:?}"
);
}
#[test]
fn sync_with_after_async_for_keeps_end_async_for_line_marker() {
let code = compile_exec(
"\
async def f(cm, source, tgt):
with cm:
async for tgt[0] in source():
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::EndAsyncFor,
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"expected CPython-style line-marker NOP between END_ASYNC_FOR and with cleanup, got ops={ops:?}"
);
}
#[test]
fn genexpr_with_async_comprehension_element_is_async_generator() {
let code = compile_exec(
"\
async def f():
gen = ([i async for i in asynciter([1, 2])] for j in [10, 20])
return [x async for x in gen]
",
);
let genexpr = find_code(&code, "<genexpr>").expect("missing genexpr code");
let units: Vec<_> = genexpr
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
assert!(
units.windows(2).any(|window| {
let [wrap, yield_value] = window else {
return false;
};
matches!(yield_value.op, Instruction::YieldValue { .. })
&& match wrap.op {
Instruction::CallIntrinsic1 { func } => {
func.get(OpArg::new(u32::from(u8::from(wrap.arg))))
== bytecode::IntrinsicFunction1::AsyncGenWrap
}
_ => false,
}
}),
"expected CPython-style ASYNC_GEN_WRAP before genexpr yield, got units={units:?}"
);
}
#[test]
fn async_comprehension_propagates_coroutine_to_enclosing_genexpr_like_cpython() {
let symbol_table = scan_program_symbol_table(
"\
async def f():
gen = ([i async for i in asynciter([1, 2])] for j in [10, 20])
return [x async for x in gen]
",
);
let genexpr =
find_symbol_table(&symbol_table, "genexpr").expect("missing genexpr symbol table");
assert!(genexpr.is_generator, "expected genexpr symbol table");
assert!(
genexpr.is_coroutine,
"CPython symtable_handle_comprehension() propagates non-generator async comprehension ste_coroutine to the enclosing genexpr"
);
}
#[test]
fn nested_module_scope_dictcomp_symbols_are_local() {
let symbol_table = scan_program_symbol_table(
"\
deoptmap = {
specialized: base
for base, family in _specializations.items()
for specialized in family
}
",
);
for name in ["base", "family", "specialized"] {
let symbol = symbol_table
.lookup(&name.into())
.unwrap_or_else(|| panic!("missing module symbol {name}"));
assert_eq!(
symbol.scope,
SymbolScope::Local,
"expected module-scope inlined comprehension symbol {name} to be Local, got {symbol:?}"
);
}
let comp = symbol_table
.inlined_comprehension_blocks
.first()
.expect("missing comprehension symbol table");
assert!(comp.comp_inlined, "expected comprehension to be inlined");
for name in ["base", "family", "specialized"] {
let symbol = comp
.lookup(&name.into())
.unwrap_or_else(|| panic!("missing comprehension symbol {name}"));
assert_eq!(
symbol.scope,
SymbolScope::Local,
"expected comprehension symbol {name} to be Local, got {symbol:?}"
);
}
}
#[test]
fn nested_module_scope_dictcomp_uses_fast_locals() {
let code = compile_exec(
"\
deoptmap = {
specialized: base
for base, family in _specializations.items()
for specialized in family
}
",
);
let ops: Vec<_> = code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.iter()
.any(|op| matches!(op, Instruction::StoreFastStoreFast { .. })),
"expected outer target unpack to use STORE_FAST_STORE_FAST, got ops={ops:?}"
);
assert!(
ops.iter().any(|op| matches!(
op,
Instruction::StoreFastLoadFast { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)),
"expected inner target/store-use path to use fast locals, got ops={ops:?}"
);
assert!(
ops.iter()
.filter(|op| matches!(op, Instruction::LoadName { .. }))
.count()
<= 1,
"unexpected extra LOAD_NAME ops in nested inlined comprehension, got ops={ops:?}"
);
assert!(
ops.iter()
.filter(|op| matches!(op, Instruction::StoreName { .. }))
.count()
<= 1,
"unexpected extra STORE_NAME ops in nested inlined comprehension, got ops={ops:?}"
);
}
#[test]
fn module_scope_inlined_comprehension_keeps_outer_iter_as_name_lookup() {
let code = compile_exec(
"\
path_separators = ['/']
_pathseps_with_colon = {f':{s}' for s in path_separators}
",
);
let ops: Vec<_> = code
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let load_name_path = ops
.windows(2)
.any(|window| matches!(window, [Instruction::LoadName { .. }, Instruction::GetIter]));
assert!(
load_name_path,
"expected outer iterable to stay a NAME lookup before GET_ITER, got ops={ops:?}"
);
assert!(
!ops.windows(2).any(|window| matches!(
window,
[
Instruction::LoadFast { .. } | Instruction::LoadFastCheck { .. },
Instruction::GetIter
]
)),
"module local outer iterable should not become a fast local, got ops={ops:?}"
);
assert!(
ops.iter().any(|op| matches!(
op,
Instruction::StoreFastLoadFast { .. } | Instruction::StoreFast { .. }
)),
"comprehension target should still use fast locals, got ops={ops:?}"
);
}
#[test]
fn function_scope_inlined_comprehension_restore_keeps_swap_before_duplicate_store() {
let code = compile_exec(
"\
def f():
a = [1 for a in [0]]
return 1
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| matches!(
window,
[
Instruction::PopIter,
Instruction::Swap { .. },
Instruction::StoreFast { .. },
Instruction::StoreFast { .. }
]
)),
"expected PopIter/SWAP 2/STORE_FAST/STORE_FAST restore tail, got ops={ops:?}"
);
}
#[test]
fn inlined_comprehension_namedexpr_target_stays_parent_fast_local() {
let code = compile_exec(
"\
def f(seq, emit):
return [(x, y) for x in seq if (y := emit(x))]
",
);
let f = find_code(&code, "f").expect("missing f code");
assert!(f.varnames.iter().any(|name| name == "y"));
assert!(!f.cellvars.iter().any(|name| name == "y"));
assert!(!f.freevars.iter().any(|name| name == "y"));
assert!(
!f.names.iter().any(|name| name == "y"),
"inlined comprehension namedexpr target should not use NAME ops, got names={:?}",
f.names
);
}
#[test]
fn inlined_comprehension_namedexpr_varnames_match_cpython_order() {
let code = compile_exec(
"\
def f():
def spam(a):
return a
input_data = [1, 2, 3]
res = [(x, y, x / y) for x in input_data if (y := spam(x)) > 0]
return res
",
);
let f = find_code(&code, "f").expect("missing f code");
assert_eq!(
f.varnames.iter().map(String::as_str).collect::<Vec<_>>(),
vec!["spam", "input_data", "x", "y", "res"]
);
}
#[test]
fn global_namedexpr_in_inlined_comprehension_saves_fast_slot() {
let code = compile_exec(
"\
def f(seq, value):
global G
[G := value for _ in seq]
",
);
let f = find_code(&code, "f").expect("missing f code");
assert!(f.varnames.iter().any(|name| name == "G"));
assert!(f.instructions.iter().any(|unit| match unit.op {
Instruction::LoadFastAndClear { var_num } => {
let idx = var_num.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.varnames[usize::from(idx)] == "G"
}
_ => false,
}));
assert!(f.instructions.iter().any(|unit| match unit.op {
Instruction::StoreGlobal { namei } => {
let idx = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(idx).unwrap()] == "G"
}
_ => false,
}));
}
#[test]
fn namedexpr_copy_uses_namedexpr_location_like_cpython() {
let code = compile_exec(
"\
def outer():
a = 10
def spam():
nonlocal a
(a := 20)
",
);
let spam = find_code(&code, "spam").expect("missing spam code");
// CPython 3.14 NamedExpr_kind emits COPY at LOC(named expression),
// between visiting the value and visiting the target.
assert_eq!(
spam.linetable.as_ref(),
&[0xf8, 0x80, 0x00, 0xe0, 0x0e, 0x10, 0x88, 0x17, 0x8b, 0x11,]
);
}
#[test]
fn genexpr_namedexpr_target_is_cell_not_fast_local() {
let code = compile_exec(
"\
def f(seq):
a = 1
return (c := x + a for x in seq)
",
);
let f = find_code(&code, "f").expect("missing f code");
assert!(!f.varnames.iter().any(|name| name == "c"));
assert_eq!(
f.cellvars.iter().map(String::as_str).collect::<Vec<_>>(),
["a", "c"]
);
}
#[test]
fn public_cellvars_follow_cpython_localsplus_order() {
let code = compile_exec(
"\
def f():
x = 10
t = False
g = ((i, j) for i in range(x) if t for j in range(x))
[x for x in range(3)]
return g
",
);
let f = find_code(&code, "f").expect("missing f code");
assert_eq!(
f.varnames.iter().map(String::as_str).collect::<Vec<_>>(),
["g", "x"]
);
assert_eq!(
f.cellvars.iter().map(String::as_str).collect::<Vec<_>>(),
["x", "t"],
"CPython assemble.c exposes co_cellvars in localsplus order: merged local cells before non-local cells"
);
}
#[test]
fn inlined_comprehension_restore_does_not_form_store_fast_load_fast() {
let code = compile_exec(
"\
def f(e):
e[1:3] = [g(i) for i in range(2)]
def g(datadir):
files = [filename[:-4] for filename in sorted(os.listdir(datadir)) if filename.endswith('.xml')]
input_files = [filename for filename in files if filename.startswith('in')]
return files, input_files
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::Swap { .. },
Instruction::StoreFast { .. },
Instruction::LoadFastBorrow { .. },
Instruction::LoadConst { .. },
Instruction::StoreSubscr,
]
)
}),
"expected CPython-style inlined comprehension restore before slice store, got ops={ops:?}"
);
assert!(
!ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::StoreFastLoadFast { .. },
Instruction::LoadConst { .. },
Instruction::StoreSubscr,
]
)
}),
"inlined comprehension restore should not be folded into STORE_FAST_LOAD_FAST, got ops={ops:?}"
);
let g = find_code(&code, "g").expect("missing g code");
let g_ops: Vec<_> = g
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
g_ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::StoreFast { .. },
Instruction::StoreFast { .. },
]
)
}),
"expected CPython-style static swap over STORE_FAST_MAYBE_NULL restore, got ops={g_ops:?}"
);
assert!(
!g_ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::Swap { .. },
Instruction::StoreFast { .. },
Instruction::StoreFast { .. },
]
)
}),
"inlined comprehension restore should statically remove SWAP before adjacent stores, got ops={g_ops:?}"
);
}
#[test]
fn single_mode_folded_multiline_constant_does_not_leave_nops() {
let code = compile_single(
"\
(-
-
-
1)
",
);
assert!(
!code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::Nop)),
"expected folded single-mode multiline constant to drop NOP anchors, got instructions={:?}",
code.instructions
);
}
#[test]
fn folded_multiline_tuple_constant_does_not_leave_operand_nops() {
let code = compile_exec(
"\
values = (
(1 + 1j, 0 + 0j),
(1 + 1j, 0.0),
(1 + 1j, 0),
)
",
);
assert!(
!code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::Nop)),
"expected CPython nop_out-style folded tuple operands to have no surviving NOPs, got instructions={:?}",
code.instructions
);
}
#[test]
fn single_mode_returns_none_after_print_like_cpython() {
let code = compile_single("1\n");
let ops = code
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Resume { .. }))
.collect::<Vec<_>>();
assert!(
!ops.iter()
.any(|unit| matches!(unit.op, Instruction::Copy { .. })),
"CPython codegen_stmt_expr() prints and pops interactive expressions; it does not preserve the final expression as the code object's return value, got ops={ops:?}"
);
let Some(load_none) = ops.iter().rev().nth(1) else {
panic!("missing final LOAD_CONST None before RETURN_VALUE, got ops={ops:?}");
};
let Instruction::LoadConst { consti } = load_none.op else {
panic!("missing final LOAD_CONST None before RETURN_VALUE, got ops={ops:?}");
};
let constant = &code.constants[consti.get(OpArg::new(u32::from(u8::from(load_none.arg))))];
assert!(matches!(constant, ConstantData::None));
assert!(matches!(
ops.last().map(|unit| unit.op),
Some(Instruction::ReturnValue)
));
}
#[test]
fn folded_multiline_bytes_binop_does_not_leave_operand_nops() {
let code = compile_exec(
"\
def f(self, out):
self.assertIn(
b'gnu' + (b'/123' * 125) + b'/longlink' + (b'/123' * 125) + b'/longname',
out)
",
);
let f = find_code(&code, "f").expect("missing f code");
assert!(
!f.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::Nop)),
"expected CPython nop_out-style folded operands to have no surviving NOPs, got instructions={:?}",
f.instructions
);
}
#[test]
fn folded_binop_at_branch_body_start_does_not_leave_nop() {
let code = compile_exec(
"\
def f(sys):
if sys.platform == 'win32':
component = 'd' * 25
return component
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::NotTaken,
Instruction::Nop,
Instruction::LoadConst { .. }
]
)
}),
"expected CPython nop_out-style folded branch body to drop operand NOP, got ops={ops:?}",
);
}
#[test]
fn folded_iterable_at_assert_target_does_not_leave_nop() {
let code = compile_exec(
r#"
def f(caches, non_caches):
assert 1 / 3 <= caches / non_caches, "this test needs more caches!"
for show_caches in (False, True):
pass
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(2).any(|window| {
matches!(window, [Instruction::Nop, Instruction::LoadConst { .. }])
}),
"expected folded for-iterable at assert target to drop operand NOP, got ops={ops:?}",
);
}
#[test]
fn multiline_unpack_target_uses_element_locations() {
let code = compile_exec(
"\
def f(cm):
with cm as (_,
filename_2):
return filename_2
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.iter()
.any(|op| matches!(op, Instruction::StoreFastStoreFast { .. })),
"expected multiline target elements to keep separate STORE_FAST instructions, got ops={ops:?}",
);
}
#[test]
fn or_condition_in_jump_context_uses_shared_true_fallthrough() {
let code = compile_exec(
"\
def f(lines):
for line in lines:
if line.startswith('--') or not line.strip():
continue
return line
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let first_pop_jump = ops
.iter()
.find(|op| {
matches!(
op,
Instruction::PopJumpIfTrue { .. } | Instruction::PopJumpIfFalse { .. }
)
})
.copied()
.expect("missing conditional jump");
assert!(
matches!(first_pop_jump, Instruction::PopJumpIfTrue { .. }),
"expected first OR branch to jump on true into shared fallthrough, got ops={ops:?}"
);
}
#[test]
fn loop_break_bool_chain_reorders_false_path_to_jump_back() {
let code = compile_exec(
"\
def f(filters, text, category, module, lineno, defaultaction):
for item in filters:
action, msg, cat, mod, ln = item
if ((msg is None or msg.match(text)) and
issubclass(category, cat) and
(mod is None or mod.match(module)) and
(ln == 0 or lineno == ln)):
break
else:
action = defaultaction
return action
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadGlobal { .. },
]
)
}),
"expected CPython-style false path to fall through into loop jump-back, got ops={ops:?}"
);
}
#[test]
fn loop_conditional_body_keeps_duplicate_jump_back_paths() {
let code = compile_exec(
"\
def f(new, old):
for replace in ['__module__', '__name__', '__qualname__', '__doc__']:
if hasattr(old, replace):
setattr(new, replace, getattr(old, replace))
return new
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let jump_back_count = ops
.iter()
.filter(|op| {
matches!(
op,
Instruction::JumpBackward { .. } | Instruction::JumpBackwardNoInterrupt { .. }
)
})
.count();
assert!(
jump_back_count >= 2,
"expected separate false-path and body jump-back blocks, got ops={ops:?}"
);
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadGlobal { .. },
]
)
}),
"expected false path to jump back before body, got ops={ops:?}"
);
}
#[test]
fn try_loop_inner_if_keeps_duplicate_jump_back_paths() {
let code = compile_exec(
"\
def f(config, logging):
handlers = config.get('handlers', {})
for name in handlers:
if name not in logging._handlers:
raise ValueError('missing')
else:
try:
handler = logging._handlers[name]
handler_config = handlers[name]
level = handler_config.get('level', None)
if level:
handler.setLevel(logging._checkLevel(level))
except Exception as e:
raise ValueError('bad') from e
loggers = config.get('loggers', {})
for name in loggers:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::EndFor,
]
)
}),
"expected CPython-style separate body and false-path jump-back blocks, got ops={ops:?}"
);
}
#[test]
fn try_loop_nested_bool_tail_keeps_duplicate_jump_back_paths() {
let code = compile_exec(
"\
def f(obj, flags, writer, value, Error):
while obj.running:
try:
if value == 0:
return
elif obj.ready and obj.active and value == 1:
obj.work()
if flags.verbose and obj.chatty:
writer.write('trace')
elif value == 2:
obj.other()
else:
obj.default()
except Error as e:
obj.running = False
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadConst { .. } | Instruction::LoadSmallInt { .. },
]
)
}),
"expected CPython-style body and both bool false-path jump-back blocks, got ops={ops:?}"
);
}
#[test]
fn nested_continue_shares_backedge_with_fallthrough_body() {
let code = compile_exec(
"\
def f(names, show_empty, keywords, args_buffer, args, cls, object, level):
for name in names:
value = getattr(cls, name)
if not show_empty:
if value == []:
field_type = cls._field_types.get(name, object)
if getattr(field_type, '__origin__', ...) is list:
if not keywords:
args_buffer.append(repr(value))
continue
if not keywords:
args.extend(args_buffer)
args_buffer = []
value, simple = _format(value, level)
if keywords:
args.append('%s=%s' % (name, value))
else:
args.append(value)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache | Instruction::NotTaken))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
]
)
}),
"expected CPython-style shared continue backedge before outer condition, got ops={ops:?}"
);
assert!(
!ops.windows(2).any(|window| {
matches!(
window,
[
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"unexpected duplicated continue/backedge jumps, got ops={ops:?}"
);
}
#[test]
fn line_bearing_loop_if_false_backedge_keeps_body_before_jump_back() {
let code = compile_exec(
"\
def f(self, replacement_pairs):
for n, d in [(19, '%OC'), (2, '%Ow')]:
if self.LC_alt_digits is None:
s = str(n)
replacement_pairs.append((s, d))
if n < 10:
replacement_pairs.append((s[1], d))
elif len(self.LC_alt_digits) > n:
replacement_pairs.append((self.LC_alt_digits[n], d))
else:
replacement_pairs.append((d, d))
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"expected CPython-style line-bearing false target to keep body before backedge, got ops={ops:?}"
);
assert!(
!ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"unexpected no-lineno-style inverted loop-if body, got ops={ops:?}"
);
}
#[test]
fn branch_local_implicit_continue_keeps_body_before_jump_back() {
let code = compile_exec(
"\
def f(items, outer, cond, sub, out):
for x in items:
if outer:
if cond:
out.append(x)
if sub:
out.append(1)
out.append(2)
else:
out.append(3)
return out
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"expected branch-local implicit continue target to stay after the body, got ops={ops:?}"
);
assert!(
!ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"unexpected direct-loop-body implicit continue layout for branch-local target, got ops={ops:?}"
);
}
#[test]
fn boolop_continue_deduplicates_marker_jump_back() {
let code = compile_exec(
"\
def f(ws, seen, more_than):
while ws:
w = ws.pop()
if w in seen or w <= more_than:
continue
seen.add(w)
return seen
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(2).any(|window| {
matches!(
window,
[
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"expected adjacent equivalent continue backedges to be deduplicated, got ops={ops:?}"
);
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
]
)
}),
"expected CPython-style boolop continue fallthrough before body, got ops={ops:?}"
);
}
#[test]
fn loop_elif_nested_if_false_backedge_keeps_body_before_jump_back() {
let code = compile_exec(
"\
def f(keys, parse_int, d, ampm, AM, PM):
hour = minute = 0
for group_key in keys:
if group_key == 'I':
hour = parse_int(d['I'])
if ampm in ('', AM):
if hour == 12:
hour = 0
elif ampm == PM:
if hour != 12:
hour += 12
elif group_key == 'M':
minute = parse_int(d['M'])
return hour, minute
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadSmallInt { .. },
]
)
}),
"expected CPython-style nested elif body before false backedge, got ops={ops:?}"
);
assert!(
!ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadSmallInt { .. },
]
)
}),
"unexpected inverted nested elif false path before body, got ops={ops:?}"
);
assert!(
ops.windows(15).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadSmallInt { .. },
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadSmallInt { .. },
Instruction::BinaryOp { .. },
Instruction::StoreFast { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"expected CPython-style duplicated body/false loop exits for nested elif, got ops={ops:?}"
);
}
#[test]
fn loop_nested_if_before_elif_keeps_body_before_false_backedge() {
let code = compile_exec(
"\
def f(keys, parse_int, found_dict, locale_time):
hour = minute = 0
for group_key in keys:
if group_key == 'I':
hour = parse_int(found_dict['I'])
ampm = found_dict.get('p', '').lower()
if ampm in ('', locale_time.am_pm[0]):
if hour == 12:
hour = 0
elif ampm == locale_time.am_pm[1]:
if hour != 12:
hour += 12
elif group_key == 'M':
minute = parse_int(found_dict['M'])
return hour, minute
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadSmallInt { .. },
Instruction::StoreFast { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"expected CPython-style nested if body before false backedge, got ops={ops:?}"
);
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadSmallInt { .. },
]
)
}),
"unexpected inverted nested if body after false backedge, got ops={ops:?}"
);
}
#[test]
fn elif_pass_before_raise_keeps_line_bearing_forward_jump() {
let code = compile_exec(
"\
def f(entries, path, self):
if entries == ['.mh_sequences']:
os.remove(os.path.join(path, '.mh_sequences'))
elif entries == []:
pass
else:
raise NotEmptyError('Folder not empty: %s' % self._path)
os.rmdir(path)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpForward { .. },
Instruction::LoadGlobal { .. },
]
)
}),
"expected CPython-style pass branch forward jump before raise body, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::LoadGlobal { .. },
]
)
}),
"unexpected inverted pass branch before raise body, got ops={ops:?}"
);
}
#[test]
fn loop_multiblock_conditional_body_keeps_body_before_jump_back() {
let code = compile_exec(
"\
def f(random, d, f):
for dummy in range(100):
k = random.choice('abc')
if random.random() < 0.2:
if k in d:
del d[k]
del f[k]
else:
v = random.choice((1, 2))
d[k] = v
f[k] = v
check(f[k], v)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrowLoadFastBorrow { .. },
Instruction::DeleteSubscr,
]
)
}),
"expected CPython-style multi-block body before false jump-back, got ops={ops:?}"
);
}
#[test]
fn loop_not_conditional_body_threads_true_path_to_jump_back() {
let code = compile_exec(
"\
def f(xs):
for x in xs:
if not x:
g(x)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadGlobal { .. },
]
)
}),
"expected CPython-style true path to jump back before not-body, got ops={ops:?}"
);
}
#[test]
fn loop_not_in_conditional_body_threads_true_path_to_jump_back() {
let code = compile_exec(
"\
def f(native, array):
for k in native:
if k not in 'bBhHiIlLfd':
del array[k]
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
]
)
}),
"expected CPython-style true path to jump back before not-in body, got ops={ops:?}"
);
}
#[test]
fn while_implicit_continue_body_after_jumpback_for_boolop_call_arg() {
let code = compile_exec(
"\
def f(source, state, verbose, nested):
items = []
itemsappend = items.append
sourcematch = source.match
while True:
itemsappend(parse(source, state, verbose, nested + 1,
not nested and not items))
if not sourcematch('|'):
break
if not nested:
verbose = state.flags & 64
return verbose
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"expected CPython-style while implicit-continue jumpback before conditional body, got ops={ops:?}"
);
}
#[test]
fn multiblock_elif_continue_keeps_next_test_before_backedge() {
let code = compile_exec(
"\
def f(source, state, verbose, nested, subpatternappend, start, MAXGROUPS):
sourceget = source.get
sourcematch = source.match
while True:
this = source.next
sourceget()
if this == '(':
if sourcematch('?'):
char = sourceget()
if char in '=!<':
dir = 1
if char == '<':
char = sourceget()
if char not in '=!':
raise source.error('unknown extension ?<' + char, len(char) + 2)
dir = -1
lookbehindgroups = state.lookbehindgroups
if lookbehindgroups is None:
state.lookbehindgroups = state.groups
p = parse_sub(source, state, verbose, nested + 1)
if dir < 0:
if lookbehindgroups is None:
state.lookbehindgroups = None
if not sourcematch(')'):
raise source.error('missing ), unterminated subpattern', source.tell() - start)
if char == '=':
subpatternappend(('ASSERT', (dir, p)))
elif p:
subpatternappend(('ASSERT_NOT', (dir, p)))
else:
subpatternappend(('FAILURE', ()))
continue
elif char == '(':
condname = source.getuntil(')', 'group name')
if not (condname.isdecimal() and condname.isascii()):
source.checkgroupname(condname, 1)
condgroup = state.groupdict.get(condname)
if condgroup is None:
msg = 'unknown group name %r' % condname
raise source.error(msg, len(condname) + 1)
else:
condgroup = int(condname)
if not condgroup:
raise source.error('bad group number', len(condname) + 1)
if condgroup >= MAXGROUPS:
msg = 'invalid group reference %d' % condgroup
raise source.error(msg, len(condname) + 1)
state.checklookbehindgroup(condgroup, source)
item_yes = parse(source, state, verbose, nested + 1)
if source.match('|'):
item_no = parse(source, state, verbose, nested + 1)
if source.next == '|':
raise source.error('conditional backref with more than two branches')
else:
item_no = None
if not source.match(')'):
raise source.error('missing ), unterminated subpattern', source.tell() - start)
subpatternappend(('GROUPREF_EXISTS', (condgroup, item_yes, item_no)))
continue
elif char == '>':
capture = False
return
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(2).any(|window| {
matches!(
window,
[
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"expected CPython-style elif test between separate continue backedges, got ops={ops:?}"
);
}
#[test]
fn while_scope_exit_body_keeps_line_backedge_before_raise_body() {
let code = compile_exec(
"\
FLAGS = {}
TYPE_FLAGS = 0
GLOBAL_FLAGS = 0
def f(source, state, char):
sourceget = source.get
add_flags = 0
del_flags = 0
if char != '-':
while True:
flag = FLAGS[char]
if source.istext:
if char == 'L':
msg = 'bad inline flags'
raise source.error(msg)
else:
if char == 'u':
msg = 'bad inline flags'
raise source.error(msg)
add_flags |= flag
if (flag & TYPE_FLAGS) and (add_flags & TYPE_FLAGS) != flag:
msg = 'bad inline flags'
raise source.error(msg)
char = sourceget()
if char is None:
raise source.error('missing -, : or )')
if char in ')-:':
break
if char not in FLAGS:
msg = 'unknown flag' if char.isalpha() else 'missing -, : or )'
raise source.error(msg, len(char))
if char == ')':
state.flags |= add_flags
return None
if add_flags & GLOBAL_FLAGS:
raise source.error('bad inline flags: cannot turn on global flag', 1)
if char == '-':
char = sourceget()
if char is None:
raise source.error('missing flag')
if char not in FLAGS:
msg = 'unknown flag' if char.isalpha() else 'missing flag'
raise source.error(msg, len(char))
while True:
flag = FLAGS[char]
if flag & TYPE_FLAGS:
msg = 'bad inline flags'
raise source.error(msg)
del_flags |= flag
char = sourceget()
if char is None:
raise source.error('missing :')
if char == ':':
break
if char not in FLAGS:
msg = 'unknown flag' if char.isalpha() else 'missing :'
raise source.error(msg, len(char))
return add_flags, del_flags
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let cpython_style_not_in_raise_body_count = ops
.windows(6)
.filter(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
})
.count();
assert!(
cpython_style_not_in_raise_body_count >= 2,
"expected both while-loop not-in checks to put the false-path backedge before the conditional-expression raise body, got ops={ops:?}"
);
}
#[test]
fn call_body_implicit_continue_keeps_cpython_normalized_forward_jump() {
let code = compile_exec(
"\
DIGITS = '0123456789'
def f(s, sget, lappend, addgroup, this, c):
while True:
if c in DIGITS:
isoctal = False
if s.next in DIGITS:
this += sget()
if not isoctal:
addgroup(int(this[1:]), len(this) - 1)
else:
lappend(this)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
]
)
}),
"expected CPython-style forward jump over call body, got ops={ops:?}"
);
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"call body should not be moved after an implicit continue backedge, got ops={ops:?}"
);
}
#[test]
fn empty_if_end_label_preserves_cpython_return_anchor_nop() {
let code = compile_exec(
"\
SRE_FLAG_LOCALE = 1
SRE_FLAG_ASCII = 2
SRE_FLAG_UNICODE = 4
def f(src, flags):
if isinstance(src, str):
if flags & SRE_FLAG_LOCALE:
raise ValueError('cannot use LOCALE flag with a str pattern')
if not flags & SRE_FLAG_ASCII:
flags |= SRE_FLAG_UNICODE
elif flags & SRE_FLAG_UNICODE:
raise ValueError('ASCII and UNICODE flags are incompatible')
else:
if flags & SRE_FLAG_UNICODE:
raise ValueError('cannot use UNICODE flag with a bytes pattern')
if flags & SRE_FLAG_LOCALE and flags & SRE_FLAG_ASCII:
raise ValueError('ASCII and LOCALE flags are incompatible')
return flags
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::RaiseVarargs { .. },
Instruction::Nop,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ReturnValue,
]
)
}),
"expected CPython-style NOP return anchor after elif raise body, got ops={ops:?}"
);
}
#[test]
fn nested_except_normal_exit_return_uses_strong_loads() {
let code = compile_exec(
"\
LITERAL = 1
def f(source, escape):
try:
c = escape[1:2]
if c == 'N' and source.istext:
try:
c = ord(source.lookup())
except (KeyError, TypeError):
raise source.error() from None
return LITERAL, c
except ValueError:
pass
raise source.error(escape)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadGlobal { .. },
Instruction::LoadFast { .. },
Instruction::BuildTuple { .. },
Instruction::ReturnValue,
]
)
}),
"expected CPython-style strong LOAD_FAST in nested except normal-exit return, got ops={ops:?}"
);
}
#[test]
fn targeted_nop_after_prefix_for_else_uses_strong_for_tail_loads() {
let code = compile_exec(
"\
LITERAL = 1
IN = 2
NEGATE = 3
def f(items):
while True:
prefix = None
for item in items:
if not item:
break
if prefix is None:
prefix = item[0]
elif item[0] != prefix:
break
else:
for item in items:
del item[0]
continue
break
set = []
for item in items:
if len(item) != 1:
break
op, av = item[0]
if op is LITERAL:
set.append((op, av))
elif op is IN and av[0][0] is not NEGATE:
set.extend(av)
else:
break
return set
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let get_iter_idx = ops
.iter()
.rposition(|op| matches!(op, Instruction::GetIter))
.expect("missing GET_ITER");
assert!(
matches!(ops[get_iter_idx - 1], Instruction::LoadFast { .. }),
"targeted NOP after prefix for/else should use strong iterable LOAD_FAST, got ops={ops:?}"
);
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFastLoadFast { .. },
Instruction::BuildTuple { .. },
]
)
}),
"targeted NOP after prefix for/else should keep set.append tuple loads strong, got ops={ops:?}"
);
}
#[test]
fn plain_pass_before_for_tail_keeps_borrows() {
let code = compile_exec(
"\
def f(xs):
pass
for x in xs:
pass
return xs
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let get_iter_idx = ops
.iter()
.position(|op| matches!(op, Instruction::GetIter))
.expect("missing GET_ITER");
assert!(
matches!(ops[get_iter_idx - 1], Instruction::LoadFastBorrow { .. }),
"plain pass before for-tail should keep borrowed iterable load, got ops={ops:?}"
);
}
#[test]
fn targeted_nop_after_return_uses_strong_pair_call_args() {
let code = compile_exec(
"\
def f(x, d, count, inner, hi, w1, lo, w2):
if x:
return
if 0:
pass
d[count] += 1
inner(hi, w1)
del hi
inner(lo, w2)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::LoadFast { .. },
Instruction::PushNull,
Instruction::LoadFastLoadFast { .. },
]
)
}),
"targeted NOP after return should keep CPython-style strong pair call args, got ops={ops:?}"
);
let nop_idx = ops
.iter()
.position(|op| matches!(op, Instruction::Nop))
.expect("missing targeted NOP");
assert!(
!ops[nop_idx..].iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"targeted NOP tail should not reintroduce borrowed fast loads, got ops={ops:?}"
);
}
#[test]
fn loop_if_pass_uses_line_bearing_jump_back_instead_of_nop() {
let code = compile_exec(
"\
def f(x, y):
for i in x:
if y:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"expected CPython-style synthetic false-path jump-back plus body jump-back, got ops={ops:?}"
);
assert!(
!ops.iter().any(|op| matches!(op, Instruction::Nop)),
"expected pass body line to attach to loop backedge instead of leaving a NOP, got ops={ops:?}"
);
}
#[test]
fn constant_true_while_pass_keeps_loop_header_nop() {
let code = compile_exec(
"\
def f():
while 1:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(2).any(|window| {
matches!(
window,
[
Instruction::Nop,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"expected CPython-style loop-header NOP before self backedge, got ops={ops:?}"
);
}
#[test]
fn nested_if_shared_jump_back_target_is_duplicated() {
let code = compile_exec(
"\
def f(s, size, encodeSetO, encodeWhiteSpace):
inShift = True
base64bits = 0
out = []
for i, ch in enumerate(s):
if base64bits == 0:
if i + 1 < size:
ch2 = s[i + 1]
if E(ch2, encodeSetO, encodeWhiteSpace):
if B(ch2) or ch2 == '-':
out.append(b'-')
inShift = False
else:
out.append(b'-')
inShift = False
return out
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::LoadConst { .. },
Instruction::StoreFast { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
]
)
}),
"expected separate nested-if and outer-if jump-back tails, got ops={ops:?}"
);
}
#[test]
fn exception_cleanup_backedge_target_is_shared() {
let code = compile_exec(
"\
def f(enum_class, value, Flag, int_type, is_single_bit):
try:
try:
enum_member = enum_class[value]
except TypeError:
raise KeyError
except KeyError:
if Flag is None or not issubclass(enum_class, Flag):
enum_class.names.append(value)
elif (
Flag is not None
and issubclass(enum_class, Flag)
and isinstance(value, int_type)
and is_single_bit(value)
):
enum_class.names.append(value)
enum_class.add(enum_member)
return enum_member
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::PopExcept,
Instruction::JumpBackwardNoInterrupt { .. }
| Instruction::JumpBackward { .. },
Instruction::Reraise { .. },
]
)
}),
"expected CPython-style shared exception cleanup backedge before reraise, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::PopExcept,
Instruction::JumpBackwardNoInterrupt { .. }
| Instruction::JumpBackward { .. },
Instruction::PopExcept,
Instruction::JumpBackwardNoInterrupt { .. }
| Instruction::JumpBackward { .. },
]
)
}),
"exception cleanup backedge should be shared, not duplicated per conditional edge, got ops={ops:?}"
);
}
#[test]
fn protected_loop_conditional_keeps_forward_body_entry() {
let code = compile_exec(
"\
def outer(it, C1):
def f():
for x in it:
try:
if C1:
yield 2
except OSError:
pass
return f
",
);
let outer = find_code(&code, "outer").expect("missing outer code");
let f = find_code(outer, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadSmallInt { .. },
Instruction::YieldValue { .. },
Instruction::Resume { .. },
Instruction::PopTop,
]
)
}),
"expected protected conditional to keep CPython-style forward body entry, got ops={ops:?}"
);
}
#[test]
fn nested_except_false_path_duplicates_pop_except_jump_back_tail() {
let code = compile_exec(
"\
def f(it, C3):
for x in it:
try:
X = 3
except OSError:
try:
if C3:
X = 4
except OSError:
pass
return 42
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::LoadSmallInt { .. },
Instruction::StoreFast { .. },
Instruction::PopExcept,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::PopExcept,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"expected CPython-style duplicated false-path exit tail, got ops={ops:?}"
);
}
#[test]
fn more_nested_except_false_paths_duplicate_all_jump_back_tails() {
let code = compile_exec(
"\
def f(it, C3, C4):
for x in it:
try:
X = 3
except OSError:
try:
if C3:
if C4:
X = 4
except OSError:
try:
if C3:
if C4:
X = 5
except OSError:
pass
return 42
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(8).any(|window| {
matches!(
window,
[
Instruction::LoadSmallInt { .. },
Instruction::StoreFast { .. },
Instruction::PopExcept,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::PopExcept,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::PopExcept,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"expected CPython-style duplicated nested false-path exit tails, got ops={ops:?}"
);
}
#[test]
fn no_wraparound_jump_keeps_forward_hop_before_loop_backedge() {
let code = compile_exec(
"\
def while_not_chained(a, b, c):
while not (a < b < c):
pass
",
);
let f = find_code(&code, "while_not_chained").expect("missing while_not_chained code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpForward { .. },
Instruction::PopTop,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"expected CPython-style no-wraparound forward hop before the loop backedge, got ops={ops:?}"
);
}
#[test]
fn nested_while_chained_compare_break_keeps_break_jump_block() {
let code = compile_exec(
"\
def f(start, self, stop, size):
while size > 0:
while True:
if start <= self.position < stop:
break
else:
self.map_index += 1
if self.map_index == len(self.map):
self.map_index = 0
length = min(size, stop - self.position)
size -= length
return size
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpForward { .. },
Instruction::PopTop,
Instruction::JumpForward { .. },
Instruction::JumpForward { .. },
]
)
}),
"expected CPython-style chained-compare success hop into the break jump block, got ops={ops:?}"
);
}
#[test]
fn while_break_else_keeps_true_edge_into_forward_break_body() {
let code = compile_exec(
"\
def f(i):
while i:
i -= 1
if i < 4:
break
else:
print('x')
print('y')
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpForward { .. },
]
)
}),
"expected CPython-style true edge into forward break body with false path falling into the loop backedge, got ops={ops:?}"
);
}
#[test]
fn nested_if_continue_reorders_false_path_to_loop_backedge() {
let code = compile_exec(
"\
def f(items, changes):
for x in items:
if not x:
if x in changes:
raise TypeError
continue
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::ContainsOp { .. },
Instruction::PopJumpIfFalse { .. },
]
)
}),
"expected nested if/continue to keep CPython-style false-edge jump-back tails, got ops={ops:?}"
);
}
#[test]
fn loop_assert_keeps_false_edge_into_raise_body() {
let code = compile_exec(
"\
def f(bytecode):
for instr, positions in zip(bytecode, bytecode.codeobj.co_positions()):
assert instr.positions == positions
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadCommonConstant { .. },
Instruction::RaiseVarargs { .. },
]
)
}),
"expected loop assert to keep CPython-style false-edge into the raise body, got ops={ops:?}"
);
}
#[test]
fn and_is_not_none_loop_guard_uses_direct_jump_back_false_path() {
let code = compile_exec(
"\
def f(code):
last_line = -2
for _, _, line in code.co_lines():
if line is not None and line != last_line:
last_line = line
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::PopJumpIfNotNone { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::CompareOp { .. },
]
)
}),
"expected CPython-style direct jump-back false path for 'is not None and ...', got ops={ops:?}"
);
}
#[test]
fn large_is_not_none_loop_guard_uses_direct_jump_back_false_path() {
let code = compile_exec(
"\
def f(cls, _FIELDS, _PARAMS):
all_frozen_bases = None
any_frozen_base = False
has_dataclass_bases = False
for b in cls.__mro__[-1:0:-1]:
base_fields = getattr(b, _FIELDS, None)
if base_fields is not None:
has_dataclass_bases = True
for field in base_fields.values():
name = field.name
if all_frozen_bases is None:
all_frozen_bases = True
current_frozen = getattr(b, _PARAMS).frozen
all_frozen_bases = all_frozen_bases and current_frozen
any_frozen_base = any_frozen_base or current_frozen
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::PopJumpIfNotNone { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadConst { .. },
Instruction::StoreFast { .. },
]
)
}),
"expected CPython-style direct jump-back false path for large 'is not None' loop body, got ops={ops:?}"
);
}
#[test]
fn continue_inside_with_keeps_line_marker_nop_before_exit_cleanup() {
let code = compile_exec(
"\
def f(it):
for func in it:
with cm():
if cond():
continue
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(9).any(|window| {
matches!(
window,
[
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::PopTop,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"expected CPython-style line-marker NOP before with-exit cleanup on continue, got ops={ops:?}"
);
}
#[test]
fn nested_async_with_normal_cleanup_drops_pop_block_nop() {
let code = compile_exec(
"\
async def foo():
async with CM():
async with CM():
raise RuntimeError
",
);
let f = find_code(&code, "foo").expect("missing foo code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::GetAwaitable { .. },
]
)
}),
"expected CPython-style async-with normal cleanup without a POP_BLOCK NOP, got ops={ops:?}"
);
assert!(
!ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::GetAwaitable { .. },
]
)
}),
"unexpected POP_BLOCK NOP before async-with normal cleanup, got ops={ops:?}"
);
}
#[test]
fn async_with_try_finally_before_outer_sync_with_cleanup_keeps_anchor_nop() {
let code = compile_exec(
"\
async def foo(self):
with self.assertRaises(ZeroDivisionError):
async with timeout():
try:
try:
raise ValueError
finally:
await sleep(1)
finally:
try:
raise KeyError
finally:
1 / 0
after()
",
);
let f = find_code(&code, "foo").expect("missing foo code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::Copy { .. },
Instruction::PopExcept,
Instruction::Reraise { .. },
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
]
)
}),
"expected CPython-style async-with after-block NOP before outer sync-with cleanup, got ops={ops:?}"
);
}
#[test]
fn nested_terminal_with_keeps_outer_cleanup_target_nop() {
let code = compile_exec(
"\
def f():
with a():
with b():
raise E()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::Copy { .. },
Instruction::PopExcept,
Instruction::Reraise { .. },
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
]
)
}),
"expected CPython-style outer with-exit target NOP after terminal nested with cleanup, got ops={ops:?}"
);
}
#[test]
fn nested_nonterminal_with_drops_outer_cleanup_target_nop() {
let code = compile_exec(
"\
def f():
with a():
with b():
x()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::Copy { .. },
Instruction::PopExcept,
Instruction::Reraise { .. },
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
]
)
}),
"unexpected outer with-exit target NOP for nested with with normal fallthrough, got ops={ops:?}"
);
}
#[test]
fn nested_terminal_with_before_successor_drops_after_block_nop() {
let code = compile_exec(
"\
def f(a, b, c):
with a:
with b:
raise c
c()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::Copy { .. },
Instruction::PopExcept,
Instruction::Reraise { .. },
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
]
)
}),
"CPython falls through from the terminal inner-with cleanup to the following statement without an after-block NOP, got ops={ops:?}"
);
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::Copy { .. },
Instruction::PopExcept,
Instruction::Reraise { .. },
Instruction::Nop,
Instruction::LoadFast { .. } | Instruction::LoadFastBorrow { .. },
]
)
}),
"unexpected inner with after-block NOP before following statement, got ops={ops:?}"
);
}
#[test]
fn try_loop_elif_places_return_before_orelse_tail() {
let code = compile_exec(
"\
def f(source, suggest, tb, s):
if source is not None:
try:
tb = tb
except Exception:
suggest = False
tb = None
if tb is not None:
for frame in tb:
s += frame
elif suggest:
s += 'x'
return s
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let has_direct_return = ops.windows(8).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ReturnValue,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
]
)
});
let has_nop_anchored_return = ops.windows(9).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::Nop,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ReturnValue,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
]
)
});
assert!(
has_direct_return || has_nop_anchored_return,
"expected CPython-style duplicated return between loop exit and elif tail, got ops={ops:?}"
);
}
#[test]
fn constant_false_while_else_deopts_post_else_borrows() {
let code = compile_exec(
"\
def f(self):
x = 0
while 0:
x = 1
else:
x = 2
self.assertEqual(x, 2)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let assert_idx = ops
.iter()
.position(|op| matches!(op, Instruction::LoadAttr { .. }))
.expect("missing assertEqual call");
let window = &ops[assert_idx.saturating_sub(1)..(assert_idx + 3).min(ops.len())];
assert!(
matches!(
window,
[
Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFast { .. },
..
]
),
"expected post-else assertEqual call to use plain LOAD_FAST, got ops={window:?}"
);
}
#[test]
fn single_unpack_assignment_disables_constant_collection_folding() {
let code = compile_exec("a, b, c = 1, 2, 3\n");
assert!(
!code.instructions.iter().any(|unit| {
matches!(unit.op, Instruction::UnpackSequence { .. })
|| matches!(unit.op, Instruction::LoadConst { .. })
&& matches!(
code.constants.get(usize::from(u8::from(unit.arg))),
Some(ConstantData::Tuple { .. })
)
}),
"single unpack assignment should keep builder form for later lowering, got ops={:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
assert!(
code.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::LoadSmallInt { .. }))
.count()
>= 3,
"expected individual constant loads before unpack-target stores, got ops={:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn four_item_unpack_assignment_folds_tuple_constant_like_cpython() {
let code = compile_exec("a, b, c, d = 1, 2, 3, 4\n");
assert!(
code.instructions.iter().any(|unit| {
matches!(unit.op, Instruction::LoadConst { .. })
&& matches!(
code.constants.get(usize::from(u8::from(unit.arg))),
Some(ConstantData::Tuple { elements }) if elements.len() == 4
)
}),
"four-item unpack assignment should fold BUILD_TUPLE before UNPACK_SEQUENCE like CPython, got ops={:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::UnpackSequence { .. })),
"four-item unpack assignment should keep UNPACK_SEQUENCE after tuple folding, got ops={:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn chained_unpack_assignment_keeps_constant_collection_folding() {
let code = compile_exec("(a, b) = c = d = (1, 2)\n");
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadConst { .. })),
"chained unpack assignment should keep tuple constant, got ops={:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::UnpackSequence { .. })),
"chained unpack assignment should still unpack the copied tuple, got ops={:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn constant_true_assert_skips_message_nested_scope() {
let code = compile_exec("assert 1, (lambda x: x + 1)\n");
assert_eq!(
code.constants
.iter()
.filter(|constant| matches!(constant, ConstantData::Code { .. }))
.count(),
0,
"constant-true assert should not compile the skipped message lambda"
);
assert!(
!code
.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. })),
"constant-true assert should be elided, got ops={:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn constant_false_assert_uses_direct_raise_shape() {
let code = compile_exec("assert 0, (lambda x: x + 1)\n");
assert!(
!code.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::ToBool
| Instruction::PopJumpIfTrue { .. }
| Instruction::PopJumpIfFalse { .. }
)
}),
"constant-false assert should use direct raise shape, got ops={:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. })),
"constant-false assert should still raise, got ops={:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
assert_eq!(
code.constants
.iter()
.filter(|constant| matches!(constant, ConstantData::Code { .. }))
.count(),
1,
"constant-false assert should still compile the message lambda"
);
}
#[test]
fn constant_unary_positive_and_invert_fold() {
let code = compile_exec("x = +1\nx = ~1\n");
assert!(
!code.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::CallIntrinsic1 { .. } | Instruction::UnaryInvert
)
}),
"constant unary ops should fold away, got ops={:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn bool_invert_is_not_const_folded() {
let code = compile_exec("x = ~True\n");
assert!(
code.instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::UnaryInvert)),
"~bool should remain unfurled to match CPython, got ops={:?}",
code.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn optimized_assert_preserves_nested_scope_order() {
compile_exec_optimized(
"\
class S:
def f(self, sequence):
_formats = [self._types_mapping[type(item)] for item in sequence]
_list_len = len(_formats)
assert sum(len(fmt) <= 8 for fmt in _formats) == _list_len
_recreation_codes = [self._extract_recreation_code(item) for item in sequence]
",
);
}
#[test]
fn optimized_assert_with_nested_scope_in_first_iter() {
// First iterator of a comprehension is evaluated in the enclosing
// scope, so nested scopes inside it (the generator here) must also
// be consumed when the assert is optimized away.
compile_exec_optimized(
"\
def f(items):
assert [x for x in (y for y in items)]
return [x for x in items]
",
);
}
#[test]
fn optimized_assert_with_lambda_defaults() {
// Lambda default values are evaluated in the enclosing scope,
// so nested scopes inside defaults must be consumed.
compile_exec_optimized(
"\
def f(items):
assert (lambda x=[i for i in items]: x)()
return [x for x in items]
",
);
}
#[test]
fn try_else_nested_scopes_keep_subtable_cursor_aligned() {
let code = compile_exec(
"\
try:
import missing_mod
except ImportError:
def fallback():
return 0
else:
def impl():
return reversed('abc')
",
);
assert!(
find_code(&code, "fallback").is_some(),
"missing fallback code"
);
let impl_code = find_code(&code, "impl").expect("missing impl code");
assert!(
impl_code.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadGlobal { .. } | Instruction::LoadName { .. }
)
}),
"expected impl to compile global name access, got ops={:?}",
impl_code
.instructions
.iter()
.map(|unit| unit.op)
.collect::<Vec<_>>()
);
}
#[test]
fn nested_try_else_multi_resume_join_keeps_strong_load_fast_tail() {
let code = compile_exec(
"\
def f(msg):
s = ''
try:
import a
except Exception:
suggest = False
tb = None
else:
try:
suggest = not t()
tb = g(msg)
except Exception:
suggest = False
tb = None
if tb is not None:
for frame in tb:
s += frame
elif suggest:
s += 'y'
return s
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let tail_start = ops
.iter()
.position(|op| matches!(op, Instruction::PopJumpIfNone { .. }))
.expect("missing tail POP_JUMP_IF_NONE")
.saturating_sub(1);
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &ops[tail_start..handler_start];
assert!(
!tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected nested try/except else-resume tail to keep strong LOAD_FAST ops, got tail={tail:?}"
);
assert!(
tail.iter()
.any(|op| matches!(op, Instruction::LoadFastLoadFast { .. })),
"expected loop body to keep LOAD_FAST_LOAD_FAST in the resume tail, got tail={tail:?}"
);
}
#[test]
fn protected_conditional_tail_keeps_strong_load_fast() {
let code = compile_exec(
"\
def f(m, class_name, category, warning_base):
try:
cat = getattr(m, class_name)
except AttributeError:
raise ValueError(category)
if not issubclass(cat, warning_base):
raise TypeError(category)
return cat
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let tail_start = ops
.iter()
.position(|op| matches!(op, Instruction::StoreFast { .. }))
.expect("missing STORE_FAST cat");
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &ops[tail_start + 1..handler_start];
assert!(
!tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected protected conditional tail to keep strong LOAD_FAST ops, got tail={tail:?}"
);
assert!(
tail.iter()
.any(|op| matches!(op, Instruction::LoadFastLoadFast { .. })),
"expected protected tail to keep LOAD_FAST_LOAD_FAST for issubclass args, got tail={tail:?}"
);
}
#[test]
fn nonresuming_protected_conditional_tail_keeps_strong_load_fast() {
let code = compile_exec(
"\
def f(href, parse='xml'):
try:
data = XINCLUDE[href]
except KeyError:
raise OSError('resource not found')
if parse == 'xml':
data = ET.XML(data)
return data
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let tail_start = ops
.iter()
.position(|op| matches!(op, Instruction::StoreFast { .. }))
.expect("missing protected STORE_FAST data");
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &ops[tail_start + 1..handler_start];
assert!(
!tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected non-resuming protected conditional tail to keep strong LOAD_FAST ops, got tail={tail:?}"
);
}
#[test]
fn optional_nonresuming_protected_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(b):
if type(b) is not bytes:
try:
b = bytes(memoryview(b))
except TypeError:
raise TypeError(f'bad {type(b).__name__}') from None
if b:
sink(b)
return len(b)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let b_index = f
.varnames
.iter()
.position(|name| name.as_str() == "b")
.expect("missing b varname");
let store_b = instructions
.iter()
.position(|unit| match unit.op {
Instruction::StoreFast { var_num } => {
usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg))))) == b_index
}
_ => false,
})
.expect("missing protected STORE_FAST b");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &instructions[store_b + 1..handler_start];
assert!(
tail.iter()
.filter(|unit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))))
== b_index
}
_ => false,
})
.all(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })),
"optional protected tail should keep CPython-style borrowed b loads, got tail={tail:?}"
);
}
#[test]
fn handled_except_conditional_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self):
try:
if self.active:
self.step()
if self.waiter is not None and self.pending is None:
self.waiter.set_result(None)
except ConnectionResetError as exc:
self.close(exc)
except OSError as exc:
self.fail(exc, 'x')
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let waiter_idx = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "waiter"
}
_ => false,
})
.expect("missing waiter LOAD_ATTR");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &instructions[waiter_idx.saturating_sub(1)..handler_start];
assert!(
tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"handled-except conditional tail should keep borrowed loads, got tail={tail:?}"
);
assert!(
!tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"handled-except conditional tail should not force strong LOAD_FAST, got tail={tail:?}"
);
}
#[test]
fn handled_except_else_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self, fut=None):
try:
if self.closed:
return
item = self.queue.popleft()
self.size -= len(item)
if self.addr is not None:
self.future = self.loop.send(self.sock, item)
else:
self.future = self.loop.sendto(self.sock, item, addr=item)
except OSError as exc:
self.protocol.error_received(exc)
except Exception as exc:
self.fatal(exc, 'x')
else:
self.future.add_done_callback(self.loop_writing)
self.resume()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let done_callback_idx = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "add_done_callback"
}
_ => false,
})
.expect("missing add_done_callback LOAD_ATTR");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &instructions[done_callback_idx.saturating_sub(3)..handler_start];
assert!(
tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"handled-except else tail should keep borrowed loads, got tail={tail:?}"
);
assert!(
!tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"handled-except else tail should not force strong LOAD_FAST, got tail={tail:?}"
);
}
#[test]
fn reraising_handler_with_handled_returns_keeps_borrow() {
let code = compile_exec(
"\
def f(self, fut=None):
try:
if fut is not None:
fut.result()
if self.future is not fut:
return
fut = self.reader.recv(self.sock, 4096)
except CancelledError:
return
except (SystemExit, KeyboardInterrupt):
raise
except BaseException as exc:
self.handle({'exception': exc, 'loop': self})
else:
self.future = fut
fut.add_done_callback(self.loop_reading)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let recv_idx = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "recv"
}
_ => false,
})
.expect("missing recv LOAD_ATTR");
let done_callback_idx = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "add_done_callback"
}
_ => false,
})
.expect("missing add_done_callback LOAD_ATTR");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail =
&instructions[recv_idx.saturating_sub(3)..handler_start.min(done_callback_idx + 3)];
assert!(
tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"handler chain with handled returns should keep borrowed warm/else loads, got tail={tail:?}"
);
assert!(
!tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. }
)
}),
"handler chain with handled returns should not force strong warm/else loads, got tail={tail:?}"
);
}
#[test]
fn with_protected_conditional_tail_without_exception_match_keeps_borrow() {
let code = compile_exec(
"\
def f(self, cm, p, platform):
with cm:
if p.returncode != 0:
if platform.machine() == 'x86_64':
p.check_returncode()
else:
self.skipTest(f'could not compile indirect function: {p}')
done()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let attr_load_uses_borrow = |name: &str| {
let attr_idx = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == name
}
_ => false,
})
.unwrap_or_else(|| panic!("missing {name} attr load"));
matches!(
instructions
.get(attr_idx.saturating_sub(1))
.map(|unit| unit.op),
Some(Instruction::LoadFastBorrow { .. })
)
};
assert!(
attr_load_uses_borrow("check_returncode"),
"plain with-protected conditional tail should keep borrowed p load, got instructions={instructions:?}"
);
assert!(
attr_load_uses_borrow("skipTest"),
"plain with-protected conditional tail should keep borrowed self load, got instructions={instructions:?}"
);
}
#[test]
fn listcomp_cleanup_predecessor_does_not_deopt_following_conditional_tail() {
let code = compile_exec(
"\
def f(self, compile_snippet):
sizes = [compile_snippet(i).co_stacksize for i in range(2, 5)]
if len(set(sizes)) != 1:
import dis, io
out = io.StringIO()
dis.dis(compile_snippet(1), file=out)
self.fail('%s\\n%s' % (sizes, out.getvalue()))
",
);
let f = find_code(&code, "f").expect("missing f code");
let has_strong_load = |name: &str| {
f.instructions.iter().any(|unit| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
})
};
let has_borrow_load = |name: &str| {
f.instructions.iter().any(|unit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
})
};
for name in ["sizes", "io", "dis", "compile_snippet", "out", "self"] {
assert!(
has_borrow_load(name),
"expected listcomp-following conditional tail to borrow {name}, got instructions={:?}",
f.instructions
);
}
for name in ["sizes", "io", "dis", "compile_snippet", "out", "self"] {
assert!(
!has_strong_load(name),
"listcomp cleanup predecessor should not force strong LOAD_FAST for {name}, got instructions={:?}",
f.instructions
);
}
}
#[test]
fn handler_resume_loop_conditional_tail_keeps_strong_load_fast() {
let code = compile_exec(
"\
def f(self):
is_utf8 = (self.ENCODING == 'utf-8')
encode_errors = 'surrogateescape' if is_utf8 else 'strict'
strings = list(self.BYTES_STRINGS)
for text in self.STRINGS:
try:
encoded = text.encode(self.ENCODING, encode_errors)
if encoded not in strings:
strings.append(encoded)
except UnicodeEncodeError:
encoded = None
if is_utf8:
encoded2 = text.encode(self.ENCODING, 'surrogatepass')
if encoded2 != encoded:
strings.append(encoded2)
for encoded in strings:
self.consume(encoded)
",
);
let f = find_code(&code, "f").expect("missing f code");
let has_strong_load = |name: &str| {
f.instructions.iter().any(|unit| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
})
};
let has_borrow_load = |name: &str| {
f.instructions.iter().any(|unit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
})
};
for name in ["is_utf8", "text", "self", "strings", "encoded2"] {
assert!(
has_strong_load(name),
"expected handler-resume loop tail to use strong LOAD_FAST for {name}, got instructions={:?}",
f.instructions
);
}
assert!(
f.instructions.iter().any(|unit| match unit.op {
Instruction::LoadFastLoadFast { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == "encoded2"
&& f.varnames[usize::from(right)] == "encoded"
}
_ => false,
}),
"expected encoded2/encoded comparison to use strong LOAD_FAST_LOAD_FAST, got instructions={:?}",
f.instructions
);
assert!(
!f.instructions.iter().any(|unit| match unit.op {
Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == "encoded2"
&& f.varnames[usize::from(right)] == "encoded"
}
_ => false,
}),
"handler-resume loop tail should not borrow encoded2/encoded comparison, got instructions={:?}",
f.instructions
);
assert!(
has_borrow_load("strings"),
"expected later loop/list uses outside the deopt tail to keep borrowing strings"
);
}
#[test]
fn handler_resume_while_conditional_tail_keeps_borrow_load_fast() {
let code = compile_exec(
"\
def f(value):
items = []
while value:
try:
token, value = parse(value)
items.append(token)
except Error:
token, value = recover(value)
items.append(token)
if value and value[0] != ',':
item = items[-1]
token, value = recover(value)
item.extend(token)
if value and value[0] == ',':
items.append(',')
value = value[1:]
return items, value
",
);
let f = find_code(&code, "f").expect("missing f code");
let handler_start = f
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_region = &f.instructions[..handler_start];
let is_strong_local_load = |unit: &CodeUnit| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
matches!(
f.varnames[usize::from(var_num.get(arg))].as_str(),
"value" | "items" | "token" | "item"
)
}
Instruction::LoadFastLoadFast { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
let left = f.varnames[usize::from(left)].as_str();
let right = f.varnames[usize::from(right)].as_str();
matches!(left, "value" | "items" | "token" | "item")
|| matches!(right, "value" | "items" | "token" | "item")
}
_ => false,
};
assert!(
!normal_region.iter().any(is_strong_local_load),
"while-loop handler resume tail should keep CPython-style borrowed normal loads, got instructions={:?}",
f.instructions
);
}
#[test]
fn multi_handler_resume_while_tail_keeps_borrow_load_fast() {
let code = compile_exec(
"\
def f(value):
items = []
while value and value[0] != ';':
try:
token, value = parse(value)
items.append(token)
except Error:
leader = None
if value[0] in leaders:
leader, value = recover(value)
if not value or value[0] in ',;':
items.append(leader)
else:
token, value = recover_invalid(value)
if leader is not None:
token[:0] = [leader]
items.append(token)
elif value[0] == ',':
items.append(',')
else:
token, value = recover_invalid(value)
if leader is not None:
token[:0] = [leader]
items.append(token)
if value and value[0] not in ',;':
item = items[-1]
token, value = recover_invalid(value)
item.extend(token)
if value and value[0] == ',':
items.append(',')
value = value[1:]
return items, value
",
);
let f = find_code(&code, "f").expect("missing f code");
let handler_start = f
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_region = &f.instructions[..handler_start];
assert!(
normal_region.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected multi-handler while tail to keep borrowed normal loads, got instructions={:?}",
f.instructions
);
assert!(
!normal_region
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"multi-handler while tail should not be deoptimized to strong LOAD_FAST, got instructions={:?}",
f.instructions
);
}
#[test]
fn multi_handler_resume_before_with_keeps_with_body_borrows() {
let code = compile_exec(
"\
def f(self, input, cm):
try:
self.stdin.flush()
except BrokenPipeError:
pass
except ValueError:
if not self.stdin.closed:
raise
if not input:
self.stdin.close()
with cm() as selector:
if self.stdin and self._input:
selector.register(self.stdin, EVENT_WRITE)
while selector.get_map():
ready = selector.select()
for key in ready:
self._fileobj2output[key].append(key)
return self.stdin
",
);
let f = find_code(&code, "f").expect("missing f code");
let handler_start = f
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let warm_path = &f.instructions[..handler_start];
let self_load_is = |unit: &CodeUnit, borrowed: bool| match unit.op {
Instruction::LoadFast { var_num } if !borrowed => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == "self"
}
Instruction::LoadFastBorrow { var_num } if borrowed => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == "self"
}
_ => false,
};
assert!(
warm_path.iter().any(|unit| self_load_is(unit, true)),
"expected multi-handler resume before with-body to keep borrowed self loads, got warm_path={warm_path:?}"
);
assert!(
!warm_path.iter().any(|unit| self_load_is(unit, false)),
"multi-handler resume must not deopt with-body self loads to strong LOAD_FAST, got warm_path={warm_path:?}"
);
}
#[test]
fn suppressing_with_and_typed_except_resume_loop_method_tail_keeps_strong_load_fast() {
let code = compile_exec(
"\
def f(proc, text):
try:
with proc.stdin as pipe:
try:
pipe.write(text)
except KeyboardInterrupt:
pass
except OSError:
pass
while True:
try:
proc.wait()
break
except KeyboardInterrupt:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let load_name = |unit: &CodeUnit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
let wait_receiver_is_strong = f.instructions.windows(2).any(|window| {
matches!(window[0].op, Instruction::LoadFast { .. })
&& load_name(&window[0]) == Some("proc")
&& matches!(window[1].op, Instruction::LoadAttr { .. })
});
assert!(
wait_receiver_is_strong,
"CPython keeps proc.wait() receiver strong after suppressing-with and typed-except loop resumes, got instructions={:?}",
f.instructions
);
}
#[test]
fn handler_break_join_loop_body_and_tail_keep_strong_load_fast() {
let code = compile_exec(
"\
def f(function, stem):
result = []
state = 0
while True:
try:
next = function(stem, state)
except Exception:
break
if not isinstance(next, str):
break
result.append(next)
state += 1
result.sort()
return result
",
);
let f = find_code(&code, "f").expect("missing f code");
let load_name = |unit: &CodeUnit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let body_method_call_uses_strong_loads = instructions.windows(4).any(|window| {
matches!(window[0].op, Instruction::LoadFast { .. })
&& load_name(window[0]) == Some("result")
&& matches!(window[1].op, Instruction::LoadAttr { .. })
&& matches!(window[2].op, Instruction::LoadFast { .. })
&& load_name(window[2]) == Some("next")
&& matches!(window[3].op, Instruction::Call { .. })
});
assert!(
body_method_call_uses_strong_loads,
"CPython keeps result.append(next) loads strong after an except-break join, got instructions={:?}",
f.instructions
);
let body_update_uses_strong_load = instructions.windows(3).any(|window| {
matches!(window[0].op, Instruction::LoadFast { .. })
&& load_name(window[0]) == Some("state")
&& matches!(window[1].op, Instruction::LoadSmallInt { .. })
&& matches!(window[2].op, Instruction::BinaryOp { .. })
});
assert!(
body_update_uses_strong_load,
"CPython keeps state += 1 load strong after an except-break join, got instructions={:?}",
f.instructions
);
let tail_method_call_uses_strong_load = instructions.windows(2).any(|window| {
matches!(window[0].op, Instruction::LoadFast { .. })
&& load_name(window[0]) == Some("result")
&& matches!(window[1].op, Instruction::LoadAttr { .. })
});
assert!(
tail_method_call_uses_strong_load,
"CPython keeps result.sort() receiver strong after an except-break join, got instructions={:?}",
f.instructions
);
let return_uses_strong_load = instructions.windows(2).any(|window| {
matches!(window[0].op, Instruction::LoadFast { .. })
&& load_name(window[0]) == Some("result")
&& matches!(window[1].op, Instruction::ReturnValue)
});
assert!(
return_uses_strong_load,
"CPython keeps post-loop result return strong after an except-break join, got instructions={:?}",
f.instructions
);
}
#[test]
fn handler_resume_to_loop_header_keeps_loop_header_borrows() {
let code = compile_exec(
"\
def f(value, Phrase, get_word, errors, ENDS, DOT):
phrase = Phrase()
try:
token, value = get_word(value)
phrase.append(token)
except errors.HeaderParseError:
phrase.defects.append(errors.InvalidHeaderDefect('bad'))
while value and value[0] not in ENDS:
if value[0] == '.':
phrase.append(DOT)
phrase.defects.append(errors.ObsoleteHeaderDefect('dot'))
value = value[1:]
else:
token, value = get_word(value)
phrase.append(token)
return phrase, value
",
);
let f = find_code(&code, "f").expect("missing f code");
let load_name = |unit: &CodeUnit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let loop_header_uses_borrow = instructions.windows(3).any(|window| {
matches!(window[0].op, Instruction::LoadFastBorrow { .. })
&& load_name(window[0]) == Some("value")
&& matches!(window[1].op, Instruction::ToBool)
&& matches!(window[2].op, Instruction::PopJumpIfFalse { .. })
});
assert!(
loop_header_uses_borrow,
"CPython keeps loop-header value test borrowed after a pre-loop handler resumes to that header, got instructions={:?}",
f.instructions
);
let return_pair_uses_borrow = instructions.windows(3).any(|window| {
matches!(
window[0].op,
Instruction::LoadFastBorrowLoadFastBorrow { .. }
) && matches!(window[1].op, Instruction::BuildTuple { .. })
&& matches!(window[2].op, Instruction::ReturnValue)
});
assert!(
return_pair_uses_borrow,
"CPython keeps phrase/value return pair borrowed after a pre-loop handler resumes to the loop header, got instructions={:?}",
f.instructions
);
}
#[test]
fn reraising_except_loop_break_tail_keeps_post_loop_borrows() {
let code = compile_exec(
"\
def f(flag=1, count=0):
value = 2
while value:
count += 1
try:
if flag and value == 1:
flag -= 1
break
value -= 1
continue
except:
raise
return count > 2 or value != 1
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let return_idx = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::ReturnValue))
.expect("missing return");
let tail = &instructions[return_idx.saturating_sub(12)..return_idx];
let load_name = |unit: &CodeUnit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
for name in ["count", "value"] {
assert!(
tail.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })
&& load_name(unit) == Some(name)),
"post-loop condition should borrow {name}, got tail={tail:?}"
);
assert!(
!tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })
&& load_name(unit) == Some(name)),
"post-loop condition should not deopt {name} to LOAD_FAST, got tail={tail:?}"
);
}
}
#[test]
fn try_except_continue_keeps_try_line_nop_before_continue_jump() {
let code = compile_exec(
"\
def f(done=False):
while not done:
done = True
try:
continue
except:
done = False
return done
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
assert!(
instructions.windows(2).any(|window| matches!(
window,
[
CodeUnit {
op: Instruction::Nop,
..
},
CodeUnit {
op: Instruction::JumpBackward { .. },
..
}
]
)),
"try/except continue should keep CPython-style try-line NOP before continue jump, got instructions={instructions:?}"
);
}
#[test]
fn for_else_pass_keeps_line_marker_after_pop_iter() {
let code = compile_exec(
"\
def f():
for item in ():
pass
else:
pass
marker = 1
return marker
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
assert!(
instructions.windows(2).any(|window| matches!(
window,
[
CodeUnit {
op: Instruction::PopIter,
..
},
CodeUnit {
op: Instruction::Nop,
..
}
]
)),
"for-else pass should keep CPython-style else-line NOP after POP_ITER, got instructions={instructions:?}"
);
}
#[test]
fn folded_if_chain_after_previous_chain_keeps_final_elif_line_marker() {
let code = compile_exec(
"\
def f():
if 0: pass
elif 0: pass
if 0: pass
elif 0: pass
elif 0: pass
elif 0: pass
else: pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let nop_count = f
.instructions
.iter()
.filter(|unit| matches!(unit.op, Instruction::Nop))
.count();
assert_eq!(
nop_count, 6,
"folded if chains should preserve CPython-style line-marker NOPs, got instructions={:?}",
f.instructions
);
}
#[test]
fn handler_resume_before_later_loop_keeps_borrowed_tail_loads() {
let code = compile_exec(
"\
def f(msg, category):
s = 'x'
if msg.line is None:
try:
import linecache
line = linecache.getline(msg.filename, msg.lineno)
except Exception:
line = None
linecache = None
else:
line = msg.line
if line:
line = line.strip()
s += ' %s\\n' % line
if msg.source is not None:
try:
import tracemalloc
except Exception:
suggest_tracemalloc = False
tb = None
else:
try:
suggest_tracemalloc = not tracemalloc.is_tracing()
tb = tracemalloc.get_object_traceback(msg.source)
except Exception:
suggest_tracemalloc = False
tb = None
if tb is not None:
for frame in tb:
s += frame.filename
elif suggest_tracemalloc:
s += category
return s
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let is_borrow_load = |unit: &&bytecode::CodeUnit, name: &str| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
};
assert!(
ops.windows(5).any(|window| {
is_borrow_load(&window[0], "line")
&& matches!(window[1].op, Instruction::ToBool)
&& matches!(window[2].op, Instruction::PopJumpIfFalse { .. })
&& matches!(window[3].op, Instruction::NotTaken)
&& is_borrow_load(&window[4], "line")
}),
"handler resume before a later independent loop should preserve CPython-style borrowed line loads, got instructions={:?}",
f.instructions
);
assert!(
ops.windows(2)
.filter(|window| {
is_borrow_load(&window[0], "tracemalloc")
&& matches!(window[1].op, Instruction::LoadAttr { .. })
})
.count()
>= 2,
"independent later loop should not deopt tracemalloc loads before the loop, got instructions={:?}",
f.instructions
);
assert!(
ops.windows(2).any(|window| {
is_borrow_load(&window[0], "s") && matches!(window[1].op, Instruction::ReturnValue)
}),
"final return should keep CPython-style borrowed s load, got instructions={:?}",
f.instructions
);
}
#[test]
fn async_early_return_send_tail_uses_strong_load_fast_after_entry() {
let code = compile_exec(
"\
class C:
async def _sock_sendfile_native(self, sock, file, offset, count):
try:
fileno = file.fileno()
except (AttributeError, io.UnsupportedOperation) as err:
raise exceptions.SendfileNotAvailableError('not a regular file')
try:
fsize = os.fstat(fileno).st_size
except OSError:
raise exceptions.SendfileNotAvailableError('not a regular file')
blocksize = count if count else fsize
if not blocksize:
return 0
blocksize = min(blocksize, 0xffff_ffff)
end_pos = min(offset + count, fsize) if count else fsize
offset = min(offset, fsize)
total_sent = 0
try:
while True:
blocksize = min(end_pos - offset, blocksize)
if blocksize <= 0:
return total_sent
await self._proactor.sendfile(sock, file, offset, blocksize)
offset += blocksize
total_sent += blocksize
finally:
if total_sent > 0:
file.seek(offset)
",
);
let f = find_code(&code, "_sock_sendfile_native").expect("missing method code");
let names_for_unit = |unit: &bytecode::CodeUnit| -> Vec<String> {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
vec![f.varnames[usize::from(var_num.get(arg))].to_string()]
}
Instruction::LoadFastLoadFast { var_nums }
| Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
let (left, right) = var_nums.get(arg).indexes();
vec![
f.varnames[usize::from(left)].to_string(),
f.varnames[usize::from(right)].to_string(),
]
}
_ => Vec::new(),
}
};
let borrowed_names: Vec<_> = f
.instructions
.iter()
.filter_map(|unit| match unit.op {
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. } => Some(names_for_unit(unit)),
_ => None,
})
.collect();
assert_eq!(
borrowed_names,
vec![vec!["file".to_owned()]],
"only the initial file.fileno() receiver should borrow, got instructions={:?}",
f.instructions
);
let has_strong_name = |name: &str| {
f.instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. }
) && names_for_unit(unit).iter().any(|loaded| loaded == name)
})
};
for name in [
"fileno",
"count",
"blocksize",
"offset",
"fsize",
"end_pos",
"total_sent",
"file",
"self",
"sock",
] {
assert!(
has_strong_name(name),
"async early-return send tail should use strong LOAD_FAST for {name}, got instructions={:?}",
f.instructions
);
}
}
#[test]
fn async_with_return_await_after_early_return_keeps_borrow_load_fast() {
let code = compile_exec(
"\
async def wait_for(fut, timeout):
if timeout is not None and timeout <= 0:
fut = ensure_future(fut)
if fut.done():
return fut.result()
await cancel_and_wait(fut)
try:
return fut.result()
except CancelledError as exc:
raise TimeoutError from exc
async with timeout_cm(timeout):
return await fut
",
);
let f = find_code(&code, "wait_for").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let loaded_names = |unit: &bytecode::CodeUnit| -> Vec<String> {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
match unit.op {
Instruction::LoadFastBorrow { var_num } => {
vec![f.varnames[usize::from(var_num.get(arg))].to_string()]
}
Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
let (left, right) = var_nums.get(arg).indexes();
vec![
f.varnames[usize::from(left)].to_string(),
f.varnames[usize::from(right)].to_string(),
]
}
_ => Vec::new(),
}
};
let borrowed_names: Vec<_> = f
.instructions
.iter()
.filter_map(|unit| {
let names = loaded_names(unit);
(!names.is_empty()).then_some(names)
})
.collect();
assert!(
borrowed_names
.iter()
.flatten()
.any(|name| name == "timeout"),
"CPython preserves borrowed timeout loads in async-with setup, got instructions={:?}",
f.instructions
);
assert!(
ops.windows(4).any(|window| {
matches!(window[0].op, Instruction::LoadFastBorrow { .. })
&& loaded_names(window[0]).iter().any(|name| name == "fut")
&& matches!(window[1].op, Instruction::LoadAttr { namei }
if f.names[usize::try_from(namei.get(OpArg::new(u32::from(u8::from(window[1].arg)))).name_idx()).unwrap()] == "result")
&& matches!(window[2].op, Instruction::Call { .. })
&& matches!(window[3].op, Instruction::ReturnValue)
}),
"CPython preserves borrowed fut loads for direct try-return method calls, got instructions={:?}",
f.instructions
);
}
#[test]
fn async_nested_try_finally_except_after_await_return_uses_strong_loads() {
let code = compile_exec(
"\
async def f(a, b, c, h):
try:
try:
await sleep(1)
finally:
h()
except E:
pass
await sleep(0)
return a, b, c
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let return_idx = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::ReturnValue))
.expect("missing return");
let tail = &ops[return_idx.saturating_sub(4)..return_idx];
assert!(
tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. }
)
}),
"nested try/finally inside try/except leaves CPython's empty normal-exit block before the next await, so return loads stay strong, got tail={tail:?}",
);
assert!(
tail.iter().all(|unit| {
!matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"nested try/finally inside try/except should not borrow final return loads, got tail={tail:?}",
);
}
#[test]
fn async_conditional_raise_finally_except_after_await_return_uses_strong_pair() {
let code = compile_exec(
"\
async def f(self, asyncio, sys, task, timeout_handle, sleep):
timed_out = False
structured_block_finished = False
outer_code_reached = False
try:
try:
await asyncio.sleep(sleep)
structured_block_finished = True
finally:
timeout_handle.cancel()
if (
timed_out
and task.uncancel() == 0
and type(sys.exception()) is asyncio.CancelledError
):
raise TimeoutError
except TimeoutError:
self.assertTrue(timed_out)
outer_code_reached = True
await asyncio.sleep(0)
return timed_out, structured_block_finished, outer_code_reached
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let return_idx = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::ReturnValue))
.expect("missing return");
let tail = &ops[return_idx.saturating_sub(5)..return_idx];
assert!(
tail.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastLoadFast { .. })),
"conditional-raise finally inside try/except should keep CPython-style strong final return pair after await, got tail={tail:?}",
);
assert!(
tail.iter()
.all(|unit| !matches!(unit.op, Instruction::LoadFastBorrowLoadFastBorrow { .. })),
"conditional-raise finally inside try/except should not borrow final return pair after await, got tail={tail:?}",
);
}
#[test]
fn try_else_attribute_probe_end_allows_following_loads_borrow() {
let code = compile_exec(
"\
def f(self):
args = (1,)
try:
getstate = self.__getstate__
except AttributeError:
dict = None
else:
dict = getstate()
if dict:
return args, dict
return args
",
);
let f = find_code(&code, "f").expect("missing function code");
let pair_arg = {
let args = f
.varnames
.iter()
.position(|name| name == "args")
.and_then(|idx| u8::try_from(idx).ok())
.expect("missing args local");
let dict = f
.varnames
.iter()
.position(|name| name == "dict")
.and_then(|idx| u8::try_from(idx).ok())
.expect("missing dict local");
(args << 4) | dict
};
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
assert!(
ops.iter().any(|unit| matches!(
unit.op,
Instruction::LoadFastBorrowLoadFastBorrow { .. }
) && u8::from(unit.arg) == pair_arg),
"CPython 3.14 optimize_load_fast() borrows args/dict after the try/else attribute-probe end; got ops={ops:?}",
);
}
#[test]
fn protected_import_tail_keeps_strong_load_fast() {
let code = compile_exec(
"\
def f(s, size, pos, errors):
message = 'x'
look = pos
try:
import unicodedata
except ImportError:
return None
if look < size and chr(s[look]) == '{':
while look < size and chr(s[look]) != '}':
look += 1
if look > pos + 1 and look < size and chr(s[look]) == '}':
message = 'y'
return message
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let import_idx = ops
.iter()
.position(|op| matches!(op, Instruction::ImportName { .. }))
.expect("missing IMPORT_NAME");
let protected_tail = &ops[import_idx + 1..];
assert!(
!protected_tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected protected import tail to keep strong LOAD_FAST ops, got tail={protected_tail:?}"
);
assert!(
protected_tail
.iter()
.any(|op| matches!(op, Instruction::LoadFastLoadFast { .. })),
"expected protected import tail to keep LOAD_FAST_LOAD_FAST ops, got tail={protected_tail:?}"
);
}
#[test]
fn nested_protected_import_tail_keeps_strong_load_fast() {
let code = compile_exec(
"\
def f(self, mode, compresslevel):
try:
try:
import zlib
except ImportError:
raise RuntimeError from None
self.zlib = zlib
self.crc = zlib.crc32(b'')
if mode == 'r':
self.exception = zlib.error
self._init_read_gz()
else:
self._init_write_gz(compresslevel)
except:
self.closed = True
raise
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let import_idx = ops
.iter()
.position(|op| matches!(op, Instruction::ImportName { .. }))
.expect("missing IMPORT_NAME");
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let nested_protected_tail = &ops[import_idx + 1..handler_start];
assert!(
!nested_protected_tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"CPython keeps strong LOAD_FAST ops after a nested protected import tail, got tail={nested_protected_tail:?}"
);
assert!(
nested_protected_tail
.iter()
.any(|op| matches!(op, Instruction::LoadFastLoadFast { .. })),
"expected nested protected import tail to keep LOAD_FAST_LOAD_FAST, got tail={nested_protected_tail:?}"
);
}
#[test]
fn unprotected_import_before_with_keeps_borrow() {
let code = compile_exec(
"\
def f(self, document):
from xml.etree import ElementInclude
document = self.xinclude_loader('C1.xml')
with self.assertRaises(OSError) as cm:
ElementInclude.include(document, self.xinclude_loader)
self.assertEqual(str(cm.exception), 'resource not found')
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let import_idx = ops
.iter()
.position(|op| matches!(op, Instruction::ImportName { .. }))
.expect("missing IMPORT_NAME");
let handler_start = ops
.iter()
.position(|op| matches!(op, Instruction::PushExcInfo))
.expect("missing handler entry");
let warm_path = &ops[import_idx + 1..handler_start];
assert!(
warm_path.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected unprotected import before with-block to keep LOAD_FAST_BORROW ops, got warm_path={warm_path:?}"
);
assert!(
warm_path
.iter()
.any(|op| matches!(op, Instruction::LoadFastBorrowLoadFastBorrow { .. })),
"expected with body arguments to keep LOAD_FAST_BORROW_LOAD_FAST_BORROW, got warm_path={warm_path:?}"
);
}
#[test]
fn from_import_after_conditional_store_join_uses_strong_prefix_loads() {
let code = compile_exec(
"\
def f(x):
if x is None:
x = 'a'
y = x.rpartition('.')[0]
from pkgutil import get_importer
return y
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let import_from_idx = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::ImportFrom { .. }))
.expect("missing IMPORT_FROM");
let rpartition_idx = ops[..import_from_idx]
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "rpartition"
}
_ => false,
})
.expect("missing rpartition LOAD_ATTR");
assert!(
matches!(ops[rpartition_idx - 1].op, Instruction::LoadFast { .. }),
"CPython optimize_load_fast() keeps the conditional-store join receiver strong before IMPORT_FROM, got ops={ops:?}"
);
}
#[test]
fn plain_import_after_conditional_store_join_keeps_borrow_prefix_loads() {
let code = compile_exec(
"\
def f(x):
if x is None:
x = 'a'
y = x.rpartition('.')[0]
import pkgutil
return y
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let import_name_idx = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::ImportName { .. }))
.expect("missing IMPORT_NAME");
let rpartition_idx = ops[..import_name_idx]
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "rpartition"
}
_ => false,
})
.expect("missing rpartition LOAD_ATTR");
assert!(
matches!(
ops[rpartition_idx - 1].op,
Instruction::LoadFastBorrow { .. }
),
"plain import after conditional-store join should keep CPython-style borrowed receiver, got ops={ops:?}"
);
}
#[test]
fn unprotected_prefix_before_try_keeps_attr_subscript_borrow() {
let code = compile_exec(
"\
def f():
import sys, getopt
usage = f'usage: {sys.argv[0]}'
try:
opts, args = getopt.getopt(sys.argv[1:], 'h')
except getopt.error as msg:
sys.stdout = sys.stderr
print(msg)
print(usage)
sys.exit(2)
return usage
",
);
let f = find_code(&code, "f").expect("missing f code");
let first_argv_idx = f
.instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "argv"
}
_ => false,
})
.expect("missing argv attr load");
let receiver = f.instructions[..first_argv_idx]
.iter()
.rev()
.find(|unit| !matches!(unit.op, Instruction::Cache))
.expect("missing argv receiver")
.op;
assert!(
matches!(receiver, Instruction::LoadFastBorrow { .. }),
"unprotected prefix before try should keep CPython-style LOAD_FAST_BORROW receiver, got {receiver:?}"
);
}
#[test]
fn terminal_except_inlined_comprehension_keeps_borrowed_warm_loads() {
let code = compile_exec(
r##"
def f(output):
output = re.sub(r"\[[0-9]+ refs\]", "", output)
try:
result = [
row.split("\t")
for row in output.splitlines()
if row and not row.startswith('#')
]
result.sort(key=lambda row: int(row[0]))
result = [row[1] for row in result]
return "\n".join(result)
except (IndexError, ValueError):
raise AssertionError(
"tracer produced unparsable output:\n{}".format(output)
)
"##,
);
let f = find_code(&code, "f").expect("missing f code");
let handler_start = f
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let warm_path = &f.instructions[..handler_start];
let load_fast_name = |unit: &bytecode::CodeUnit| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
let borrow_name = |unit: &bytecode::CodeUnit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
assert!(
warm_path
.iter()
.filter_map(load_fast_name)
.all(|name| name != "row" && name != "result"),
"terminal-except inlined comprehension warm path should keep CPython-style borrowed row/result loads, got warm_path={warm_path:?}"
);
for name in ["row", "result"] {
assert!(
warm_path
.iter()
.filter_map(borrow_name)
.any(|var| var == name),
"expected borrowed {name} load in terminal-except inlined comprehension warm path, got warm_path={warm_path:?}"
);
}
}
#[test]
fn outer_guarded_protected_import_keeps_borrow_tail() {
let code = compile_exec(
"\
def f(sys, os, file):
if sys.platform == 'win32':
try:
import nt
if not nt._supports_virtual_terminal():
return False
except (ImportError, AttributeError):
return False
try:
return os.isatty(file.fileno())
except OSError:
return hasattr(file, 'isatty') and file.isatty()
",
);
let f = find_code(&code, "f").expect("missing f code");
let borrows_name = |name: &str| {
f.instructions.iter().any(|unit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name
}
_ => false,
})
};
for name in ["nt", "os", "file"] {
assert!(
borrows_name(name),
"outer-guarded protected import should keep CPython-style borrow for {name}, got instructions={:?}",
f.instructions
);
}
}
#[test]
fn loop_or_break_continue_orders_break_before_backedge() {
let code = compile_exec(
"\
def f(self, quoted):
while True:
if self.state == 'x':
if self.token or (self.posix and quoted):
break
else:
continue
elif self.state == 'y':
self.consume()
x = self.a + self.b + self.c
return x
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let quoted_load = ops
.iter()
.position(|unit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == "quoted"
}
_ => false,
})
.expect("missing quoted LOAD_FAST_BORROW");
let final_cond = ops[quoted_load + 1..]
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }
)
})
.map(|idx| quoted_load + 1 + idx)
.expect("missing final conditional jump");
assert!(
matches!(ops[final_cond].op, Instruction::PopJumpIfFalse { .. }),
"expected CPython-style inverted final condition, got ops={ops:?}"
);
let break_jump_idx = ops[final_cond + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::JumpForward { .. }))
.map(|idx| final_cond + 1 + idx)
.expect("missing break jump after condition");
let jump_back_idx = ops[final_cond + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::JumpBackward { .. }))
.map(|idx| final_cond + 1 + idx)
.expect("missing continue backedge");
assert!(
break_jump_idx < jump_back_idx,
"expected break jump before continue backedge, got ops={ops:?}"
);
}
#[test]
fn backward_jump_extended_arg_accounts_for_jump_cache() {
let mut source = String::from("def f(x, items):\n while x:\n");
for _ in 0..10 {
source.push_str(" x = len(items[-1])\n");
}
for _ in 0..6 {
source.push_str(" len(items)\n");
}
source.push_str(" continue\n");
let code = compile_exec(&source);
let f = find_code(&code, "f").expect("missing f code");
assert!(
f.instructions.windows(2).any(|window| {
matches!(
(&window[0].op, &window[1].op),
(
Instruction::ExtendedArg,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. }
)
)
}),
"CPython assemble.c resolves unconditional jumps before jump offsets, so the first offset pass must include JUMP_BACKWARD's inline cache and emit EXTENDED_ARG at this boundary; got instructions={:?}",
f.instructions
);
}
#[test]
fn for_continue_before_return_orders_backedge_before_return_body() {
let code = compile_exec(
"\
def f(self):
for version in AllowedVersions:
if not version in self.capabilities:
continue
self.PROTOCOL_VERSION = version
return
raise self.error('x')
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let contains_idx = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::ContainsOp { .. }))
.expect("missing containment test");
let cond_idx = ops[contains_idx + 1..]
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }
)
})
.map(|idx| contains_idx + 1 + idx)
.expect("missing conditional jump");
assert!(
matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }),
"expected CPython-style condition targeting the return body, got ops={ops:?}"
);
let backedge_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::JumpBackward { .. }))
.map(|idx| cond_idx + 1 + idx)
.expect("missing continue backedge");
let store_attr_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| match unit.op {
Instruction::StoreAttr { namei } => {
let namei = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(namei).unwrap()].as_str() == "PROTOCOL_VERSION"
}
_ => false,
})
.map(|idx| cond_idx + 1 + idx)
.expect("missing PROTOCOL_VERSION store");
assert!(
backedge_idx < store_attr_idx,
"expected continue backedge before return body, got ops={ops:?}"
);
}
#[test]
fn while_conditional_return_orders_backedge_before_return_body() {
let code = compile_exec(
"\
def f(self, tag):
while self._get_response():
if self.tagged_commands[tag]:
return tag
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let subscript_idx = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::BinaryOp { .. }))
.expect("missing tagged_commands subscript");
let cond_idx = ops[subscript_idx + 1..]
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }
)
})
.map(|idx| subscript_idx + 1 + idx)
.expect("missing conditional jump");
assert!(
matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }),
"expected CPython-style condition targeting return body, got ops={ops:?}"
);
let backedge_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::JumpBackward { .. }))
.map(|idx| cond_idx + 1 + idx)
.expect("missing loop backedge");
let return_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::ReturnValue))
.map(|idx| cond_idx + 1 + idx)
.expect("missing return");
assert!(
backedge_idx < return_idx,
"expected loop backedge before return body, got ops={ops:?}"
);
}
#[test]
fn while_boolop_conditional_return_splits_backedges_before_return_body() {
let code = compile_exec(
"\
def f(flags, A, B, stop):
while True:
if stop:
break
if flags & A and flags & B:
return None
return flags
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(12).any(|window| {
matches!(
window,
[
Instruction::BinaryOp { .. },
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. },
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::BinaryOp { .. },
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. },
Instruction::LoadConst { .. },
]
)
}),
"boolop conditional return in a while tail should split CPython-style false backedges before the return body, got ops={ops:?}"
);
}
#[test]
fn for_break_to_return_orders_backedge_before_return() {
let code = compile_exec(
"\
def f(it):
best = 10
body = None
for prio, part in it:
if prio < best:
best = prio
body = part
if prio == 0:
break
return body
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let compare_idx = ops
.iter()
.enumerate()
.filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. }))
.nth(1)
.map(|(idx, _)| idx)
.expect("missing break comparison");
let cond_idx = ops[compare_idx + 1..]
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }
)
})
.map(|idx| compare_idx + 1 + idx)
.expect("missing break conditional jump");
assert!(
matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }),
"expected CPython-style true jump to break return path, got ops={ops:?}"
);
let jump_back_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::JumpBackward { .. }))
.map(|idx| cond_idx + 1 + idx)
.expect("missing loop backedge before break return");
let return_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::ReturnValue))
.map(|idx| cond_idx + 1 + idx)
.expect("missing break return path");
assert!(
jump_back_idx < return_idx,
"expected loop backedge before break return block, got ops={ops:?}"
);
}
#[test]
fn for_conditional_raise_orders_backedge_before_raise() {
let code = compile_exec(
"\
def f(items, limit):
found = 0
for item in items:
if item:
found += 1
if found >= limit:
raise ValueError(found)
return found
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let compare_idx = ops
.iter()
.enumerate()
.find(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. }))
.map(|(idx, _)| idx)
.expect("missing raise comparison");
let cond_idx = ops[compare_idx + 1..]
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }
)
})
.map(|idx| compare_idx + 1 + idx)
.expect("missing raise conditional jump");
assert!(
matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }),
"expected CPython-style true jump to raise path, got ops={ops:?}"
);
let jump_back_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::JumpBackward { .. }))
.map(|idx| cond_idx + 1 + idx)
.expect("missing loop backedge before raise");
let raise_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. }))
.map(|idx| cond_idx + 1 + idx)
.expect("missing raise path");
assert!(
jump_back_idx < raise_idx,
"expected loop backedge before conditional raise block, got ops={ops:?}"
);
}
#[test]
fn simple_for_conditional_raise_orders_backedge_before_raise() {
let code = compile_exec(
"\
def f(kw):
for k in ('stdout', 'check'):
if k in kw:
raise ValueError(f'{k} argument not allowed, it will be overridden.')
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. },
Instruction::LoadGlobal { .. },
]
)
}),
"expected CPython-style true jump to raise path after loop backedge, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadGlobal { .. },
]
)
}),
"unexpected conditional raise body before loop backedge, got ops={ops:?}"
);
}
#[test]
fn loop_nested_boolop_exit_keeps_cpython_backedge_line_order() {
let code = compile_exec(
"\
def f(found, value, m, done, name, renamed_variables, keep_unresolved, variables):
for _ in [0]:
if m is not None:
if found:
if '$' in value:
done[name] = value
else:
try:
value = int(value)
except ValueError:
done[name] = value.strip()
else:
done[name] = value
variables.remove(name)
if name.startswith('PY_') \\
and name[3:] in renamed_variables:
name = name[3:]
if name not in done:
done[name] = value
else:
if keep_unresolved:
done[name] = value
variables.remove(name)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops_lines: Vec<_> = f
.instructions
.iter()
.zip(&f.locations)
.filter_map(|(unit, (location, _))| {
(!matches!(unit.op, Instruction::Cache)).then_some((unit.op, location.line.get()))
})
.collect();
assert!(
ops_lines.windows(9).any(|window| {
matches!(
window,
[
(Instruction::StoreSubscr, 19),
(Instruction::JumpBackward { .. }, 19),
(Instruction::JumpBackward { .. }, 18),
(Instruction::JumpBackward { .. }, 16),
(Instruction::JumpBackward { .. }, 15),
(Instruction::JumpBackward { .. }, 4),
(
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
21
),
(Instruction::ToBool, 21),
(Instruction::PopJumpIfFalse { .. }, 21),
]
)
}),
"expected CPython-style nested boolop backedge line order before enclosing else, got ops_lines={ops_lines:?}"
);
}
#[test]
fn loop_conditional_raise_before_elif_keeps_raise_before_backedge() {
let code = compile_exec(
"\
def f(checks, missing, named):
for check in checks:
if check == 1:
if missing:
raise ValueError('x')
elif check is named:
pass
return checks
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadGlobal { .. },
Instruction::LoadConst { .. },
]
)
}),
"expected CPython-style false edge into raise body before following elif chain, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. },
]
)
}),
"unexpected loop backedge before conditional raise body in if/elif chain, got ops={ops:?}"
);
}
#[test]
fn protected_for_is_none_raise_threads_backedge_before_raise() {
let code = compile_exec(
"\
def f(stacklevel, frame, skip_file_prefixes):
try:
for x in range(stacklevel - 1):
frame = _next_external_frame(frame, skip_file_prefixes)
if frame is None:
raise ValueError
except ValueError:
frame = None
return frame
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::PopJumpIfNone { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. },
Instruction::LoadGlobal { .. },
]
)
}),
"expected protected is-None raise path to match CPython's backedge-before-raise layout, got ops={ops:?}"
);
}
#[test]
fn exception_handler_loop_conditional_raise_orders_backedge_before_raise() {
let code = compile_exec(
"\
def f(chunk, dec, i):
try:
for c in chunk:
acc = dec[c]
except TypeError:
for j, c in enumerate(chunk):
if dec[c] is None:
raise ValueError('%d' % (i + j)) from None
raise
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::BinaryOp { .. },
Instruction::PopJumpIfNone { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadGlobal { .. },
]
)
}),
"expected exception-handler loop false path to jump back before raise body, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::BinaryOp { .. },
Instruction::PopJumpIfNotNone { .. },
Instruction::NotTaken,
Instruction::LoadGlobal { .. },
]
)
}),
"unexpected exception-handler loop raise body before backedge, got ops={ops:?}"
);
}
#[test]
fn exception_handler_loop_conditional_return_orders_backedge_before_return() {
let code = compile_exec(
"\
def f(cls, value):
try:
return cls[value]
except TypeError:
for name, values in cls.items():
if value in values:
return cls[name]
for name, member in cls.items():
if value == member.value:
return cls[name]
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastLoadFast { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. },
]
)
}),
"expected exception-handler loop false path to jump back before return body, got ops={ops:?}"
);
assert!(
!ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastLoadFast { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. },
]
)
}),
"unexpected exception-handler loop return body before backedge, got ops={ops:?}"
);
}
#[test]
fn loop_if_body_keeps_fallthrough_before_implicit_continue_backedge() {
let code = compile_exec(
"\
def f(b, curr, curr_append, decoded_append, packI, curr_clear):
for x in b:
if 33 <= x <= 117:
curr_append(x)
if len(curr) == 5:
acc = 0
for x in curr:
acc = 85 * acc + (x - 33)
decoded_append(packI(acc))
curr_clear()
elif x == 122:
decoded_append(0)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadSmallInt { .. },
Instruction::StoreFast { .. },
]
)
}),
"expected CPython-style conditional body fallthrough before implicit continue backedge, got ops={ops:?}"
);
assert!(
!ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadSmallInt { .. },
Instruction::StoreFast { .. },
]
)
}),
"unexpected inverted conditional with implicit continue backedge before body, got ops={ops:?}"
);
}
#[test]
fn if_not_continue_before_conditional_listcomp_body_keeps_cpython_layout() {
let code = compile_exec(
"\
def f(data, use):
for line in data:
line = line.strip()
if not line:
continue
if line.startswith('@'):
continue
values = [use(x) for x in line]
use(values)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"if-not continue should keep CPython's forward true edge over the continue backedge, got ops={ops:?}"
);
}
#[test]
fn chained_compare_continue_does_not_duplicate_cleanup_backedge() {
let code = compile_exec(
"\
def f(items):
offsets = []
ranges = [(0, 10), (20, 30)]
for item in items:
trans_time, offset_before, offset_after = item
for dt_min, dt_max in ranges:
if trans_time is not None and not (dt_min <= trans_time <= dt_max):
continue
if offset_before not in offsets:
offsets.append(offset_before)
if offset_after not in offsets:
offsets.append(offset_after)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFast { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::ContainsOp { .. }
| Instruction::LoadFastBorrow { .. }
| Instruction::LoadFast { .. },
]
)
}),
"chained-compare continue cleanup should fall through to the following body after one backedge, got ops={ops:?}"
);
assert!(
!ops.windows(3).any(|window| {
matches!(
window,
[
Instruction::PopTop,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"chained-compare continue cleanup should not duplicate the loop backedge, got ops={ops:?}"
);
}
#[test]
fn try_else_loop_if_body_keeps_cpython_fallthrough_before_backedge() {
let code = compile_exec(
"\
def f(self, ready, selector, key, input_view, os, BrokenPipeError):
for key, events in ready:
if key.fileobj is self.stdin:
chunk = input_view[self._input_offset:self._input_offset + 1]
try:
self._input_offset += os.write(key.fd, chunk)
except BrokenPipeError:
selector.unregister(key.fileobj)
key.fileobj.close()
else:
if self._input_offset >= len(input_view):
selector.unregister(key.fileobj)
key.fileobj.close()
elif key.fileobj in (self.stdout, self.stderr):
self.read(key)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"expected CPython-style try-else if body fallthrough before loop backedge, got ops={ops:?}"
);
assert!(
!ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"unexpected inverted try-else conditional with loop backedge before body, got ops={ops:?}"
);
}
#[test]
fn try_else_after_conditional_raise_keeps_loop_if_body_before_backedge() {
let code = compile_exec(
"\
def f(seq, flag, stat, OSError, pred, SpecialFileError):
for i in seq:
try:
st = stat(i)
except OSError:
pass
else:
if pred(st.mode):
raise SpecialFileError(i)
if flag and i == 0:
x = st.real
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::StoreFast { .. },
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"try-else tail after conditional raise should keep CPython body-before-backedge layout, got ops={ops:?}"
);
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
]
)
}),
"try-else tail should not invert the inner condition before its body, got ops={ops:?}"
);
}
#[test]
fn explicit_continue_after_return_orders_return_before_backedge() {
let code = compile_exec(
"\
def f(j, n):
while j < n:
if j < 0:
return j
continue
return -1
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let compare_idx = ops
.iter()
.enumerate()
.filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. }))
.nth(1)
.map(|(idx, _)| idx)
.expect("missing inner comparison");
let cond_idx = ops[compare_idx + 1..]
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }
)
})
.map(|idx| compare_idx + 1 + idx)
.expect("missing conditional jump");
assert!(
matches!(ops[cond_idx].op, Instruction::PopJumpIfFalse { .. }),
"expected CPython-style false jump to explicit continue, got ops={ops:?}"
);
let return_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::ReturnValue))
.map(|idx| cond_idx + 1 + idx)
.expect("missing return path");
let jump_back_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::JumpBackward { .. }))
.map(|idx| cond_idx + 1 + idx)
.expect("missing explicit continue backedge");
assert!(
return_idx < jump_back_idx,
"expected return block before explicit continue backedge, got ops={ops:?}"
);
}
#[test]
fn while_break_tail_does_not_duplicate_loop_false_return_epilogue() {
let code = compile_exec(
"\
def f(waiters):
while waiters:
waiter = waiters.popleft()
if not waiter.done():
waiter.set_result(None)
break
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let returns = ops
.iter()
.filter(|unit| matches!(unit.op, Instruction::ReturnValue))
.count();
assert_eq!(
returns, 2,
"CPython codegen_while() reuses the empty anchor block for USE_LABEL(end) when orelse is empty, so only the break fallthrough and loop-false epilogues remain, got ops={ops:?}",
);
}
#[test]
fn implicit_while_tail_return_orders_backedge_before_return() {
let code = compile_exec(
"\
def f(self, j, n):
while j < n:
name, j = self.scan(j)
if j < 0:
return j
return -1
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let compare_idx = ops
.iter()
.enumerate()
.filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. }))
.nth(1)
.map(|(idx, _)| idx)
.expect("missing inner comparison");
let cond_idx = ops[compare_idx + 1..]
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }
)
})
.map(|idx| compare_idx + 1 + idx)
.expect("missing conditional jump");
assert!(
matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }),
"expected CPython-style true jump to return, got ops={ops:?}"
);
let jump_back_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::JumpBackward { .. }))
.map(|idx| cond_idx + 1 + idx)
.expect("missing implicit backedge");
let return_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::ReturnValue))
.map(|idx| cond_idx + 1 + idx)
.expect("missing return path");
assert!(
jump_back_idx < return_idx,
"expected implicit loop backedge before return block, got ops={ops:?}"
);
}
#[test]
fn branch_arm_implicit_continue_keeps_return_before_backedge() {
let code = compile_exec(
"\
def f(self, j, n, c):
while j < n:
if c == 'x':
j = self.step(j)
if j < 0:
return j
elif c == 'y':
j = j + 1
return -1
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let compare_idx = ops
.iter()
.enumerate()
.filter(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. }))
.nth(2)
.map(|(idx, _)| idx)
.expect("missing branch-arm return comparison");
let cond_idx = ops[compare_idx + 1..]
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }
)
})
.map(|idx| compare_idx + 1 + idx)
.expect("missing branch-arm conditional jump");
assert!(
matches!(ops[cond_idx].op, Instruction::PopJumpIfFalse { .. }),
"expected CPython-style false jump to branch-arm continuation, got ops={ops:?}"
);
let return_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::ReturnValue))
.map(|idx| cond_idx + 1 + idx)
.expect("missing branch-arm return path");
let jump_back_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::JumpBackward { .. }))
.map(|idx| cond_idx + 1 + idx)
.expect("missing branch-arm loop backedge");
assert!(
return_idx < jump_back_idx,
"expected branch-arm return before loop backedge, got ops={ops:?}"
);
}
#[test]
fn nested_implicit_while_tail_return_orders_backedge_before_return() {
let code = compile_exec(
"\
def f(self, rawdata, j, match):
while 1:
c = rawdata[j:j + 1]
if c in \"'\\\"\":
m = match(rawdata, j)
if not m:
return -1
j = m.end()
else:
name, j = self.scan(j)
if j < 0:
return j
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let compare_idx = ops
.iter()
.enumerate()
.rfind(|(_, unit)| matches!(unit.op, Instruction::CompareOp { .. }))
.map(|(idx, _)| idx)
.expect("missing nested tail comparison");
let cond_idx = ops[compare_idx + 1..]
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::PopJumpIfFalse { .. } | Instruction::PopJumpIfTrue { .. }
)
})
.map(|idx| compare_idx + 1 + idx)
.expect("missing nested tail conditional jump");
assert!(
matches!(ops[cond_idx].op, Instruction::PopJumpIfTrue { .. }),
"expected CPython-style true jump to nested return path, got ops={ops:?}"
);
let jump_back_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::JumpBackward { .. }))
.map(|idx| cond_idx + 1 + idx)
.expect("missing nested tail loop backedge");
let return_idx = ops[cond_idx + 1..]
.iter()
.position(|unit| matches!(unit.op, Instruction::ReturnValue))
.map(|idx| cond_idx + 1 + idx)
.expect("missing nested tail return path");
assert!(
jump_back_idx < return_idx,
"expected nested implicit loop backedge before return block, got ops={ops:?}"
);
}
#[test]
fn join_store_global_before_import_keeps_strong_load_fast() {
let code = compile_exec(
"\
def f(module=None):
global ET
if module is None:
module = pyET
ET = module
from xml.etree import ElementPath
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(2).any(|window| {
matches!(
window,
[
Instruction::LoadFast { .. },
Instruction::StoreGlobal { .. },
]
)
}),
"expected CPython-style strong LOAD_FAST before join STORE_GLOBAL followed by import, got ops={ops:?}"
);
}
#[test]
fn handler_resume_join_keeps_borrow_in_common_tail() {
let code = compile_exec(
"\
def f(p, errors, s, pos, look, final, escape_start, st):
try:
chr_codec = unicodedata.lookup('%s' % st)
except LookupError as e:
x = unicode_call_errorhandler(
errors, 'unicodeescape', 'unknown Unicode character name', s, pos - 1, look + 1
)
else:
x = chr_codec, look + 1
p.append(x[0])
pos = x[1]
if not final:
pos = escape_start
return p, pos
return unicode_call_errorhandler(
errors, 'unicodeescape', 'unknown Unicode character name', s, pos - 1, look + 1
)
",
);
let f = find_code(&code, "f").expect("missing f code");
let append_idx = f
.instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "append"
}
_ => false,
})
.expect("missing append tail");
let tail: Vec<_> = f.instructions[append_idx.saturating_sub(1)..]
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
matches!(
tail.as_slice(),
[
Instruction::LoadFastBorrow { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFastBorrow { .. },
..,
]
),
"expected handler resume common tail to start with borrowed append receiver/arg loads, got tail={tail:?}"
);
assert!(
tail.iter().any(|op| {
matches!(
op,
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastBorrow { .. }
)
}),
"expected handler resume common tail to keep borrowed LOAD_FAST ops, got tail={tail:?}"
);
}
#[test]
fn multi_handler_guarded_resume_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(a):
try:
g()
except ValueError:
pass
except TypeError:
pass
if a:
return a.x
return 0
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. },
]
)
}),
"expected guarded resume tail to keep borrowed guard/body loads, got ops={ops:?}"
);
assert!(
ops.windows(2).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::LoadAttr { .. }
]
)
}),
"expected guarded resume tail attr access to keep borrowed receiver, got ops={ops:?}"
);
}
#[test]
fn multi_handler_method_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self, xs):
for vals, expected in xs:
try:
actual = g(vals)
except OverflowError:
self.fail(expected)
except ValueError:
self.fail(expected)
self.assertEqual(actual, expected)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
let assert_equal_idx = ops
.iter()
.position(|op| matches!(op, Instruction::LoadAttr { .. }))
.expect("missing assertEqual LOAD_ATTR");
let tail = &ops[assert_equal_idx.saturating_sub(1)..];
assert!(
matches!(tail.first(), Some(Instruction::LoadFastBorrow { .. })),
"expected multi-handler method-call tail receiver to keep LOAD_FAST_BORROW, got tail={tail:?}"
);
assert!(
tail.iter()
.any(|op| matches!(op, Instruction::LoadFastBorrow { .. })),
"expected multi-handler method-call tail args to keep borrowed loads, got tail={tail:?}"
);
}
#[test]
fn named_except_cleanup_loop_header_keeps_borrow_in_for_loop() {
let code = compile_exec(
"\
def f(args):
for arg in args:
try:
_wm._setoption(arg)
except _wm._OptionError as msg:
print('Invalid -W option ignored:', msg, file=sys.stderr)
",
);
let f = find_code(&code, "f").expect("missing f code");
let attr_idx = f
.instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "_setoption"
}
_ => false,
})
.expect("missing _setoption attr load");
let window: Vec<_> = f.instructions[attr_idx + 1..]
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.take(3)
.collect();
assert!(
matches!(
window.as_slice(),
[
Instruction::LoadFastBorrow { .. },
Instruction::Call { .. },
Instruction::PopTop
]
),
"expected loop body call to keep borrowed arg load after named-except cleanup, got window={window:?}"
);
}
#[test]
fn multi_named_except_loop_header_keeps_borrow_for_normal_path() {
let code = compile_exec(
"\
def f(self):
for badval in ['illegal', -1, 1 << 32]:
class A:
def __len__(self):
return badval
try:
bool(A())
except (Exception) as e_bool:
try:
len(A())
except (Exception) as e_len:
self.assertEqual(str(e_bool), str(e_len))
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(4).any(|window| {
matches!(
window,
[
Instruction::LoadBuildClass,
Instruction::PushNull,
Instruction::LoadFastBorrow { .. },
Instruction::BuildTuple { .. },
]
)
}),
"expected class closure setup in loop header to borrow badval, got ops={ops:?}"
);
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrow { .. },
Instruction::PushNull,
Instruction::Call { .. },
Instruction::Call { .. },
Instruction::PopTop,
]
)
}),
"expected normal bool(A()) path in loop header to borrow A, got ops={ops:?}"
);
}
#[test]
fn named_except_cleanup_simple_resume_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self):
try:
1 / 0
except Exception as e:
tb = e.__traceback__
self.get_disassemble_as_string(tb.tb_frame.f_code, tb.tb_lasti)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let attr_idx = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "get_disassemble_as_string"
}
_ => false,
})
.expect("missing LOAD_ATTR for get_disassemble_as_string");
let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect();
assert!(
matches!(
ops.get(attr_idx - 1),
Some(Instruction::LoadFastBorrow { .. })
),
"expected named-except resume tail to keep borrowed self load, got ops={ops:?}"
);
assert!(
matches!(
ops.get(attr_idx + 4),
Some(Instruction::LoadFastBorrow { .. })
),
"expected named-except resume tail to keep borrowed tb load, got ops={ops:?}"
);
}
#[test]
fn named_except_cleanup_conditional_raise_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self):
try:
output = self.trace()
output = output.strip()
except (A, B, C) as fnfe:
output = str(fnfe)
if output != 'probe: success':
raise E('{} {}'.format(self.command[0], output))
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let raise_idx = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::RaiseVarargs { .. }))
.expect("missing conditional raise");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &instructions[..handler_start.min(raise_idx)];
assert!(
tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"named-except cleanup conditional raise tail should keep borrowed loads, got tail={tail:?}"
);
assert!(
!tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"named-except cleanup conditional raise tail should not force strong LOAD_FAST, got tail={tail:?}"
);
}
#[test]
fn with_suppress_named_except_resume_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(self, cm, E):
try:
with cm:
pass
except E as e:
frames = e
self.x(frames)
self.y(frames)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let first_tail_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "x"
}
_ => false,
})
.expect("missing x attr load");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &instructions[first_tail_attr.saturating_sub(1)..handler_start];
assert!(
tail.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"expected with-suppress/named-except resume tail to use strong LOAD_FAST, got tail={tail:?}"
);
assert!(
tail.iter().all(|unit| {
!matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected with-suppress/named-except resume tail not to borrow, got tail={tail:?}"
);
}
#[test]
fn with_named_except_return_value_keeps_borrow() {
let code = compile_exec(
"\
def f(self, b, BlockingIOError):
with self._write_lock:
written = len(self._write_buf)
if len(self._write_buf) > self.buffer_size:
try:
self._flush_unlocked()
except BlockingIOError as e:
if len(self._write_buf) > self.buffer_size:
overage = len(self._write_buf) - self.buffer_size
written -= overage
raise BlockingIOError(e.errno, e.strerror, written)
return written
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let with_exit_start = instructions
.windows(3)
.position(|window| {
matches!(
window,
[
CodeUnit {
op: Instruction::Swap { .. },
..
},
CodeUnit {
op: Instruction::Swap { .. },
..
},
CodeUnit {
op: Instruction::LoadConst { .. },
..
},
]
)
})
.expect("missing with-exit cleanup");
let return_value_load = instructions
.get(with_exit_start.saturating_sub(1))
.expect("missing return value load");
let arg = OpArg::new(u32::from(u8::from(return_value_load.arg)));
assert!(
matches!(
return_value_load.op,
Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(arg))] == "written"
),
"return value loaded through with-exit cleanup should keep borrowed written, got instructions={instructions:?}"
);
}
#[test]
fn with_final_conditional_return_preserves_fallthrough_cleanup_nop() {
let code = compile_exec(
"\
def f(self):
with self.lock:
if self.raw is None or self.closed:
return
self.flush()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.map(|unit| unit.op)
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::PopTop,
]
)
}),
"with fallthrough cleanup should preserve the CPython POP_BLOCK NOP, got ops={ops:?}"
);
}
#[test]
fn with_while_fallthrough_preserves_cleanup_nop() {
let code = compile_exec(
"\
def f(cm, source):
with cm as out:
s = source.read()
while s:
out.write(s)
s = source.read()
source.close()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.map(|unit| unit.op)
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
Instruction::PopTop,
]
)
}),
"with cleanup after while fallthrough should preserve the CPython POP_BLOCK NOP, got ops={ops:?}"
);
}
#[test]
fn with_for_fallthrough_drops_cleanup_nop() {
let code = compile_exec(
"\
def f(cm, xs, g):
with cm:
for x in xs:
g(x)
return None
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.map(|unit| unit.op)
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"with cleanup after for fallthrough should directly follow END_FOR/POP_ITER like CPython, got ops={ops:?}"
);
assert!(
!ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::EndFor,
Instruction::PopIter,
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"with cleanup after for fallthrough should not preserve a POP_BLOCK NOP, got ops={ops:?}"
);
}
#[test]
fn with_while_true_break_drops_cleanup_nop() {
let code = compile_exec(
"\
def f(cm, source):
with cm as out:
while True:
data = source.read()
if not data:
break
out.write(data)
source.close()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.map(|unit| unit.op)
.collect();
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"with cleanup after while True break should not preserve a POP_BLOCK NOP, got ops={ops:?}"
);
}
#[test]
fn multi_with_while_true_try_except_drops_outer_cleanup_nop() {
let code = compile_exec(
"\
def f(cm1, cm2, g, E):
with cm1, cm2:
while True:
try:
g()
except E:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.map(|unit| unit.op)
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::Copy { .. },
Instruction::PopExcept,
Instruction::Reraise { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
]
)
}),
"outer with cleanup after an infinite inner with body should follow the inner cleanup directly like CPython, got ops={ops:?}"
);
assert!(
!ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::Copy { .. },
Instruction::PopExcept,
Instruction::Reraise { .. },
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
]
)
}),
"outer with cleanup after an infinite inner with body should not keep a POP_BLOCK NOP, got ops={ops:?}"
);
}
#[test]
fn with_final_assert_preserves_cleanup_nop() {
let code = compile_exec(
"\
def f(cm, dst):
with cm:
assert not dst.closed
return dst
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.map(|unit| unit.op)
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::Call { .. },
]
)
}),
"with cleanup after a final assert should preserve CPython's POP_BLOCK NOP anchor, got ops={ops:?}"
);
}
#[test]
fn named_except_conditional_reraise_final_store_attr_keeps_borrow() {
let code = compile_exec(
"\
def f(self, fd, file, closefd, owned_fd, OSError, AttributeError, errno, os, stat, _setmode):
try:
self._closefd = closefd
self._stat_atopen = os.fstat(fd)
try:
if stat.S_ISDIR(self._stat_atopen.st_mode):
raise IsADirectoryError(errno.EISDIR, os.strerror(errno.EISDIR), file)
except AttributeError:
pass
if _setmode:
_setmode(fd, os.O_BINARY)
self.name = file
if self._appending:
try:
os.lseek(fd, 0, SEEK_END)
except OSError as e:
if e.errno != errno.ESPIPE:
raise
except:
self._stat_atopen = None
if owned_fd is not None:
os.close(owned_fd)
raise
self._fd = fd
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let load_attr_name = |unit: &&bytecode::CodeUnit, expected: &str| match unit.op {
Instruction::LoadAttr { namei } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let load_attr = namei.get(arg);
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == expected
}
_ => false,
};
let lseek_attr = instructions
.iter()
.position(|unit| load_attr_name(unit, "lseek"))
.expect("missing lseek load");
let store_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::StoreAttr { namei } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.names[usize::try_from(namei.get(arg)).unwrap()].as_str() == "_fd"
}
_ => false,
})
.expect("missing _fd store");
assert!(
matches!(
instructions
.get(lseek_attr.saturating_sub(1))
.map(|unit| unit.op),
Some(Instruction::LoadFastBorrow { .. })
),
"nested conditional reraise try body should keep borrowed os receiver, got instructions={instructions:?}"
);
assert!(
matches!(
instructions.get(lseek_attr + 1).map(|unit| unit.op),
Some(Instruction::LoadFastBorrow { .. })
),
"nested conditional reraise try body should keep borrowed fd argument, got instructions={instructions:?}"
);
assert!(
matches!(
instructions
.get(store_attr.saturating_sub(1))
.map(|unit| unit.op),
Some(Instruction::LoadFastBorrowLoadFastBorrow { .. })
),
"conditional reraise named-except final store tail should keep borrowed pair loads, got instructions={instructions:?}"
);
}
#[test]
fn with_except_else_with_resume_loop_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(self, cm, E):
with cm:
try:
g()
except E:
pass
else:
with self.z(E):
h()
for _ in support.sleeping_retry(support.SHORT_TIMEOUT, 'not ready'):
if self.x:
break
self.y()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let get_iter = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::GetIter))
.expect("missing loop iterator");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &instructions[get_iter.saturating_sub(1)..handler_start];
assert!(
tail.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"expected with except/else-with resume loop tail to use strong LOAD_FAST ops, got tail={tail:?}"
);
assert!(
tail.iter().all(|unit| {
!matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"with except/else-with resume loop tail should not borrow LOAD_FAST ops, got tail={tail:?}"
);
}
#[test]
fn final_with_try_except_resume_loop_tail_uses_strong_loads() {
let code = compile_exec(
r#"
def f(resources, valid_zones, TZPATH, os):
try:
with resources.open("r") as f:
pass
except Exception:
pass
for tz_root in TZPATH:
if not os.path.exists(tz_root):
continue
valid_zones.add(tz_root)
return valid_zones
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let get_iter = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::GetIter))
.expect("missing post-try loop iterator");
let tail = &instructions[get_iter.saturating_sub(1)..];
let load_fast_name = |unit: &&bytecode::CodeUnit| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
let borrowed_name = |unit: &&bytecode::CodeUnit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
for name in ["TZPATH", "os", "tz_root", "valid_zones"] {
assert!(
tail.iter()
.filter_map(load_fast_name)
.any(|loaded| loaded == name),
"expected CPython-style strong LOAD_FAST for {name} after final with/except resume, got tail={tail:?}",
);
assert!(
tail.iter()
.filter_map(borrowed_name)
.all(|loaded| loaded != name),
"final with/except resume loop tail should not borrow {name}, got tail={tail:?}",
);
}
}
#[test]
fn finally_ending_try_except_resume_tail_uses_strong_loads() {
let code = compile_exec(
r#"
def f(self, fobj, unlink, TESTFN, C):
try:
fobj.write(1)
finally:
fobj.close()
try:
unlink(TESTFN)
except OSError:
pass
a, b = C(2), C(3)
self.assertEqual((a, b), (1, 2))
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let assert_equal = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "assertEqual"
}
_ => false,
})
.expect("missing assertEqual load");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing exception path");
let tail = &instructions[assert_equal.saturating_sub(1)..handler_start];
let is_strong_pair = |unit: &&bytecode::CodeUnit, left_name: &str, right_name: &str| {
let Instruction::LoadFastLoadFast { var_nums } = unit.op else {
return false;
};
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == left_name
&& f.varnames[usize::from(right)] == right_name
};
assert!(
tail.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"expected finally/try-except resume tail to use strong LOAD_FAST ops, got tail={tail:?}"
);
assert!(
tail.iter().any(|unit| is_strong_pair(unit, "a", "b")),
"expected finally/try-except resume tuple to use strong LOAD_FAST_LOAD_FAST, got tail={tail:?}"
);
assert!(
tail.iter().all(|unit| {
!matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"finally/try-except resume tail should not borrow LOAD_FAST ops, got tail={tail:?}"
);
}
#[test]
fn try_finally_bare_reraise_handler_resume_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(self, os, alive_r, alive_w, address, pid):
try:
pid = g()
except:
os.close(alive_w)
raise
finally:
os.close(alive_r)
self.address = address
self.alive_w = alive_w
self.pid = pid
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let close_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "close"
}
_ => false,
})
.expect("missing close load");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing exception path");
let tail = &instructions[close_attr.saturating_sub(1)..handler_start];
assert!(
tail.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"bare-reraise try/finally resume tail should use strong LOAD_FAST ops, got tail={tail:?}"
);
assert!(
tail.iter().all(|unit| {
!matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"bare-reraise try/finally resume tail should not borrow LOAD_FAST ops, got tail={tail:?}"
);
}
#[test]
fn typed_except_return_resume_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(resource, desired_fds, max_fds):
try:
import math
except ImportError:
return None
fd_limit = resource.getrlimit(resource.RLIMIT_NOFILE)
if fd_limit < desired_fds and fd_limit < max_fds:
return desired_fds, max_fds
return fd_limit
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let getrlimit_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "getrlimit"
}
_ => false,
})
.expect("missing getrlimit load");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing exception path");
let tail = &instructions[getrlimit_attr.saturating_sub(1)..handler_start];
assert!(
tail.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"typed except-return resume tail should use strong LOAD_FAST ops, got tail={tail:?}"
);
assert!(
tail.iter().all(|unit| {
!matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"typed except-return resume tail should not borrow LOAD_FAST ops, got tail={tail:?}"
);
}
#[test]
fn resuming_except_before_try_preserves_next_try_entry_barrier() {
let code = compile_exec(
"\
def f(scan_once, s, end, _ws, _w):
try:
if s[end] in _ws:
end = _w(s, end + 1).end()
except IndexError:
pass
try:
value, end = scan_once(s, end)
except StopIteration as err:
raise ValueError(s, err.value) from None
return value, end
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let scan_once_load = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))].as_str() == "scan_once"
}
_ => false,
})
.expect("missing scan_once load");
let scan_tail = &instructions[scan_once_load..scan_once_load + 4];
assert!(
scan_tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. }
)
}),
"resuming except before another try should enter next try with strong LOAD_FAST ops, got scan_tail={scan_tail:?}"
);
assert!(
scan_tail.iter().all(|unit| {
!matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"resuming except before another try should not borrow next try entry loads, got scan_tail={scan_tail:?}"
);
}
#[test]
fn simple_except_before_try_keeps_next_try_entry_borrowed() {
let code = compile_exec(
"\
def f(scan_once, s, end):
try:
g(s, end)
except IndexError:
pass
try:
value, end = scan_once(s, end)
except StopIteration:
pass
return value, end
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let scan_once_load = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))].as_str() == "scan_once"
}
_ => false,
})
.expect("missing scan_once load");
let scan_tail = &instructions[scan_once_load..scan_once_load + 4];
assert!(
scan_tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"simple except before another try should keep next try entry borrowed, got scan_tail={scan_tail:?}"
);
assert!(
!scan_tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. }
)
}),
"simple except before another try should not force strong LOAD_FAST, got scan_tail={scan_tail:?}"
);
}
#[test]
fn loop_break_except_before_try_preserves_next_try_entry_barrier() {
let code = compile_exec(
"\
def f(scan_once, seq1, seq2, n):
for i in range(n):
try:
item1 = seq1[i]
except (TypeError, IndexError, NotImplementedError):
break
try:
item2 = seq2[i]
except (TypeError, IndexError, NotImplementedError):
break
return item1, item2
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let second_subscript = instructions
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::BinaryOp { op }
if op.get(OpArg::new(u32::from(u8::from(unit.arg))))
== oparg::BinaryOperator::Subscr
)
})
.expect("missing first subscript");
let second_subscript = instructions[second_subscript + 1..]
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::BinaryOp { op }
if op.get(OpArg::new(u32::from(u8::from(unit.arg))))
== oparg::BinaryOperator::Subscr
)
})
.map(|idx| idx + second_subscript + 1)
.expect("missing second subscript");
let scan_tail = &instructions[second_subscript.saturating_sub(2)..second_subscript];
assert!(
scan_tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. }
)
}),
"loop break except before another try should keep next try entry strong, got scan_tail={scan_tail:?}"
);
assert!(
!scan_tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"loop break except before another try should not borrow next try entry loads, got scan_tail={scan_tail:?}"
);
}
#[test]
fn plain_with_then_global_loop_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self, cm):
with cm:
self.x()
for value in ITEMS:
self.y(value)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let y_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "y"
}
_ => false,
})
.expect("missing y attr load");
assert!(
matches!(
instructions
.get(y_attr.saturating_sub(1))
.map(|unit| unit.op),
Some(Instruction::LoadFastBorrow { .. })
),
"plain with/global-loop tail should keep CPython-style borrowed self load, got instructions={instructions:?}"
);
}
#[test]
fn context_manager_for_join_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self, factory):
with factory() as e:
executor = e
self.x(e)
for t in executor._threads:
t.join()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let join_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "join"
}
_ => false,
})
.expect("missing join attr load");
assert!(
matches!(
instructions
.get(join_attr.saturating_sub(1))
.map(|unit| unit.op),
Some(Instruction::LoadFastBorrow { .. })
),
"context-manager for-join tail should keep CPython-style borrowed t load, got instructions={instructions:?}"
);
}
#[test]
fn with_except_resume_normal_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(self, cm, E):
try:
with self.assertRaises(E):
with cm:
h()
except TimeoutError:
self._fail_on_deadlock(cm)
cm.shutdown(wait=True)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let shutdown_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "shutdown"
}
_ => false,
})
.expect("missing shutdown attr load");
assert!(
matches!(
instructions
.get(shutdown_attr.saturating_sub(1))
.map(|unit| unit.op),
Some(Instruction::LoadFast { .. })
),
"with/except resume normal tail should keep CPython-style strong cm load, got instructions={instructions:?}"
);
}
#[test]
fn with_except_else_attr_subscript_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self, cm, E, obj):
try:
with cm:
pass
except E as exc:
self.x(exc)
else:
self.fail('Expected')
inner = obj.saved_details[1]
self.x(inner)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let saved_details = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "saved_details"
}
_ => false,
})
.expect("missing saved_details attr load");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &instructions[saved_details.saturating_sub(1)..handler_start];
assert!(
tail.iter().any(|unit| matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)),
"expected except-else attr-subscript tail to keep borrowed LOAD_FAST ops, got tail={tail:?}"
);
assert!(
tail.iter()
.all(|unit| !matches!(unit.op, Instruction::LoadFast { .. })),
"except-else attr-subscript tail should not be deoptimized to strong LOAD_FAST, got tail={tail:?}"
);
}
#[test]
fn with_suppress_attr_subscript_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self, cm):
stack = self.exit_stack()
with self.assertRaisesRegex(TypeError, 'the context manager'):
stack.enter_context(cm)
stack.push(cm)
self.assertIs(stack._exit_callbacks[-1][1], cm)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let exit_callbacks = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "_exit_callbacks"
}
_ => false,
})
.expect("missing _exit_callbacks attr load");
assert!(
matches!(
instructions
.get(exit_callbacks.saturating_sub(1))
.map(|unit| unit.op),
Some(Instruction::LoadFastBorrow { .. })
),
"with-suppress attr-subscript tail should keep CPython-style borrowed stack load, got instructions={instructions:?}"
);
}
#[test]
fn named_except_conditional_reraise_deopts_with_chain_tail() {
let code = compile_exec(
"\
def f(self, arc, tmp_filename, new_mode):
try:
os.chmod(tmp_filename, new_mode)
except OSError as exc:
if exc.errno == ERR:
self.skipTest()
else:
raise
with self.check_context(arc.open(), 'fully_trusted'):
self.expect_file('a')
with self.check_context(arc.open(), 'tar'):
self.expect_file('b')
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let first_check_context = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "check_context"
}
_ => false,
})
.expect("missing check_context load");
let first_handler = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.unwrap_or(instructions.len());
let warm_tail = &instructions[first_check_context.saturating_sub(1)..first_handler];
assert!(
warm_tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"expected conditional named-except reraise tail to use strong LOAD_FAST ops, got tail={warm_tail:?}"
);
assert!(
warm_tail.iter().all(|unit| {
!matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"expected all warm with-chain tail loads to stay strong after named-except reraise, got tail={warm_tail:?}"
);
}
#[test]
fn terminal_bare_reraise_successor_join_keeps_final_store_borrow() {
let code = compile_exec(
"\
def f(self, fd, appending, errno):
try:
if appending:
try:
seek(fd)
except OSError as e:
if e.errno != errno:
raise
except:
self.stat = None
raise
self._fd = fd
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let store_fd = instructions
.iter()
.position(|unit| match unit.op {
Instruction::StoreAttr { namei } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.names[usize::try_from(namei.get(arg)).unwrap()].as_str() == "_fd"
}
_ => false,
})
.expect("missing _fd STORE_ATTR");
assert!(
matches!(
instructions[store_fd - 1].op,
Instruction::LoadFastBorrowLoadFastBorrow { .. }
),
"terminal bare-reraise body successor join should keep CPython-style borrowed final store pair, got instructions={instructions:?}"
);
}
#[test]
fn terminal_except_before_with_deopts_with_body_borrows() {
let code = compile_exec(
"\
def f(self, cm):
try:
g()
except OSError:
raise Exception('skip')
with cm:
self.x()
self.y()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let first_tail_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "x"
}
_ => false,
})
.expect("missing x attr load");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let with_tail = &instructions[first_tail_attr.saturating_sub(1)..handler_start];
assert!(
with_tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"expected terminal-except before with to use strong LOAD_FAST ops, got tail={with_tail:?}"
);
assert!(
with_tail.iter().all(|unit| {
!matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"terminal-except before with should not borrow protected with body loads, got tail={with_tail:?}"
);
}
#[test]
fn terminal_except_resume_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(re, proc, unittest):
try:
version = proc.communicate()
except OSError:
raise unittest.SkipTest('x')
match = re.search('pat', version)
if match is None:
raise unittest.SkipTest(f'Unable to parse readelf version: {version}')
return int(match.group(1)), int(match.group(2))
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let search_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "search"
}
_ => false,
})
.expect("missing re.search attr load");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &instructions[search_attr.saturating_sub(1)..handler_start];
let strong_loads_name = |name: &str| {
tail.iter().any(|unit| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
Instruction::LoadFastLoadFast { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name
}
_ => false,
})
};
let borrows_name = |name: &str| {
tail.iter().any(|unit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name
}
_ => false,
})
};
for name in ["re", "version", "match"] {
assert!(
strong_loads_name(name),
"terminal-except resume tail should use strong LOAD_FAST for {name}, got tail={tail:?}"
);
assert!(
!borrows_name(name),
"terminal-except resume tail should not borrow {name}, got tail={tail:?}"
);
}
}
#[test]
fn terminal_except_conditional_return_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(param, value, quote):
try:
value.encode('ascii')
except UnicodeEncodeError:
return param
if quote:
return param
return value
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let quote_idx = instructions[..handler_start]
.iter()
.position(|unit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == "quote"
}
_ => false,
})
.expect("missing quote guard load");
let tail = &instructions[quote_idx..handler_start];
let strong_loads_name = |name: &str| {
tail.iter().any(|unit| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
})
};
let borrows_name = |name: &str| {
tail.iter().any(|unit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
})
};
for name in ["quote", "param", "value"] {
assert!(
strong_loads_name(name),
"terminal-except conditional tail should use strong LOAD_FAST for {name}, got tail={tail:?}"
);
assert!(
!borrows_name(name),
"terminal-except conditional tail should not borrow {name}, got tail={tail:?}"
);
}
}
#[test]
fn terminal_except_successor_call_tail_uses_strong_load() {
let code = compile_exec(
"\
def f(curr, decoded_append, packI, curr_clear, Error):
if len(curr) == 5:
acc = 0
for x in curr:
acc = 85 * acc + (x - 33)
try:
decoded_append(packI(acc))
except Error:
raise ValueError('overflow') from None
curr_clear()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let curr_clear_load = instructions[..handler_start]
.iter()
.rev()
.find(|unit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == "curr_clear"
}
_ => false,
})
.expect("missing curr_clear load");
assert!(
matches!(curr_clear_load.op, Instruction::LoadFast { .. }),
"terminal except successor call tail should use strong LOAD_FAST for curr_clear, got instructions={instructions:?}"
);
}
#[test]
fn loop_terminal_except_continue_if_tail_keeps_borrowed_loads() {
let code = compile_exec(
"\
def f(self, parser, opt, accum, rest, section, map, path, depth):
while rest:
rawval = rest.pop()
try:
if len(path) == 1:
opt = parser.optionxform(path[0])
v = map[opt]
elif len(path) == 2:
sect = path[0]
opt = parser.optionxform(path[1])
v = parser.get(sect, opt, raw=True)
else:
raise InterpolationSyntaxError(option, section, 'x')
except (KeyError, NoSectionError, NoOptionError):
raise InterpolationMissingOptionError(option, section, rawval, ':'.join(path)) from None
if v is None:
continue
if '$' in v:
self._interpolate_some(parser, opt, accum, v, sect, dict(parser.items(sect, raw=True)), depth + 1)
else:
accum.append(v)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let warm_path = &ops[..handler_start];
let post_try_if = warm_path
.iter()
.position(|unit| matches!(unit.op, Instruction::PopJumpIfNotNone { .. }))
.expect("missing post-try if");
let tail = &warm_path[post_try_if.saturating_sub(1)..];
let mentions_name = |unit: &CodeUnit, name: &str| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
Instruction::LoadFastLoadFast { var_nums }
| Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name
}
_ => false,
};
let borrows_name = |name: &str| {
tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
) && mentions_name(unit, name)
})
};
let strong_loads_name = |name: &str| {
tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. }
) && mentions_name(unit, name)
})
};
for name in ["v", "self", "parser", "opt", "accum", "depth"] {
assert!(
borrows_name(name),
"CPython keeps loop terminal-except continue-if tail borrowed for {name}, got tail={tail:?}"
);
assert!(
!strong_loads_name(name),
"loop terminal-except continue-if tail should not deopt {name}, got tail={tail:?}"
);
}
}
#[test]
fn method_call_try_return_handler_keeps_following_receiver_borrowed() {
let code = compile_exec(
"\
def f(charset, failobj, E):
try:
charset.encode('us-ascii')
except E:
return failobj
return charset.lower()
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let lower_attr = ops
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "lower"
}
_ => false,
})
.expect("missing lower attr");
let receiver = &ops[lower_attr - 1];
assert!(
matches!(receiver.op, Instruction::LoadFastBorrow { .. }),
"CPython codegen_try_except() does not leave a load-fast barrier after method-call try body when the handler returns, got ops={ops:?}"
);
}
#[test]
fn typed_terminal_method_call_try_deopts_successor_call_args() {
let code = compile_exec(
"\
def f(events, callback, args, self, sig, signal):
try:
signal.set_wakeup_fd(self._csock.fileno())
except ValueError:
raise RuntimeError('bad signal') from None
handle = events.Handle(callback, args, self, None)
self._signal_handlers[sig] = handle
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handle_store = ops
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::StoreFast { var_num }
if f.varnames[usize::from(
var_num.get(OpArg::new(u32::from(u8::from(unit.arg))))
)] == "handle"
)
})
.expect("missing handle store");
let handle_call_window = &ops[handle_store.saturating_sub(4)..handle_store];
assert!(
matches!(
handle_call_window,
[
bytecode::CodeUnit {
op: Instruction::LoadFastLoadFast { .. },
..
},
bytecode::CodeUnit {
op: Instruction::LoadFast { .. },
..
},
bytecode::CodeUnit {
op: Instruction::LoadConst { .. },
..
},
bytecode::CodeUnit {
op: Instruction::Call { .. },
..
},
]
),
"CPython codegen_try_except() leaves a USE_LABEL(end) continuation after the terminal typed handler; successor call args should be strong loads, got window={handle_call_window:?}; ops={ops:?}"
);
}
#[test]
fn typed_terminal_unpack_call_try_deopts_successor_call_args() {
let code = compile_exec(
"\
def f(self, OSError):
try:
request, client_address = self.get_request()
except OSError:
return
if self.verify_request(request, client_address):
self.process_request(request, client_address)
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let verify_attr = ops
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.names[usize::try_from(namei.get(arg).name_idx()).unwrap()].as_str()
== "verify_request"
}
_ => false,
})
.expect("missing verify_request attr");
let request_pair = &ops[verify_attr + 1];
assert!(
matches!(
request_pair.op,
Instruction::LoadFastLoadFast { var_nums }
if {
let arg = OpArg::new(u32::from(u8::from(request_pair.arg)));
let pair = var_nums.get(arg).as_u32();
f.varnames[(pair >> 4) as usize].as_str() == "request"
&& f.varnames[(pair & 0xF) as usize].as_str() == "client_address"
}
),
"CPython codegen_try_except() keeps successor call args strong after terminal typed unpack-call try body, got ops={ops:?}"
);
}
#[test]
fn terminal_except_following_if_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(s):
try:
t = s[1:]
d = g(s)
except ValueError:
raise ValueError('bad') from None
if t:
try:
a, b, c = h(t)
except ValueError:
raise ValueError('bad') from None
else:
if b:
x, y, z = d
if y <= 12 and z <= q(x, y):
z += 1
d = [x, y, z]
else:
a = [0]
return k(*(d + a))
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail_start = ops
.iter()
.position(|unit| {
matches!(
unit.op,
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(
var_num.get(OpArg::new(u32::from(u8::from(unit.arg))))
)] == "t"
)
})
.expect("missing post-try t load");
let tail = &ops[tail_start..handler_start];
assert!(
!tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"terminal except following conditional tail should use CPython-style strong LOAD_FAST ops, got tail={tail:?}"
);
}
#[test]
fn bare_except_internal_condition_keeps_try_body_borrows() {
let code = compile_exec(
"\
def f(buffering, raw, binary, result, BufferedReader):
try:
line_buffering = False
if buffering == 1 or buffering < 0 and raw._isatty_open_only():
buffering = -1
line_buffering = True
if buffering < 0:
buffering = max(min(raw._blksize, 8192 * 1024), 8192)
if buffering < 0:
raise ValueError('invalid buffering size')
if buffering == 0:
if binary:
return result
raise ValueError(\"can't have unbuffered text I/O\")
buffer = BufferedReader(raw, buffering)
result = buffer
if binary:
return result
return result
except:
result.close()
raise
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let warm_path = &ops[..handler_start];
let borrows_name = |name: &str| {
warm_path.iter().any(|unit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name
}
_ => false,
})
};
let strong_loads_name = |name: &str| {
warm_path.iter().any(|unit| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
Instruction::LoadFastLoadFast { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name
}
_ => false,
})
};
for name in ["buffering", "raw", "binary", "result", "BufferedReader"] {
assert!(
borrows_name(name),
"CPython keeps {name} borrowed inside the same bare-except protected region, got warm_path={warm_path:?}"
);
assert!(
!strong_loads_name(name),
"same protected-region conditional tail should not be terminal-except deoptimized for {name}, got warm_path={warm_path:?}"
);
}
}
#[test]
fn try_except_else_terminal_handler_conditional_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(self, pos, whence):
try:
pos_index = pos.__index__
except AttributeError:
raise TypeError(f'{pos!r} is not an integer')
else:
pos = pos_index()
if whence == 0:
if pos < 0:
raise ValueError(f'negative {pos!r}')
self._pos = pos
elif whence == 1:
self._pos = max(0, self._pos + pos)
return self._pos
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let warm_path = &ops[..handler_start];
let op_mentions_name = |unit: &CodeUnit, name: &str| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
Instruction::LoadFastLoadFast { var_nums }
| Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
f.varnames[usize::from(left)] == name || f.varnames[usize::from(right)] == name
}
_ => false,
};
let is_borrow_for_name = |unit: &CodeUnit, name: &str| match unit.op {
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. } => op_mentions_name(unit, name),
_ => false,
};
let is_strong_for_name = |unit: &CodeUnit, name: &str| match unit.op {
Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. } => {
op_mentions_name(unit, name)
}
_ => false,
};
assert!(
warm_path
.iter()
.any(|unit| is_borrow_for_name(unit, "pos_index")),
"try-else call should remain borrowed before the terminal-handler tail, got warm_path={warm_path:?}"
);
let tail_start = warm_path
.iter()
.position(|unit| {
is_strong_for_name(unit, "whence") || is_borrow_for_name(unit, "whence")
})
.expect("missing post-try conditional tail");
let post_try_tail = &warm_path[tail_start..];
for name in ["whence", "pos", "self"] {
assert!(
post_try_tail
.iter()
.any(|unit| is_strong_for_name(unit, name)),
"terminal except conditional tail should use strong loads for {name}, got tail={post_try_tail:?}"
);
assert!(
!post_try_tail
.iter()
.any(|unit| is_borrow_for_name(unit, name)),
"terminal except conditional tail should not borrow {name}, got tail={post_try_tail:?}"
);
}
}
#[test]
fn try_except_else_outer_join_keeps_borrowed_loads() {
let code = compile_exec(
"\
def f(self, pos=None):
if self.closed:
raise ValueError('closed')
if pos is None:
pos = self._pos
else:
try:
pos_index = pos.__index__
except AttributeError:
raise TypeError(f'{pos!r} is not an integer')
else:
pos = pos_index()
if pos < 0:
raise ValueError(f'negative {pos!r}')
del self._buffer[pos:]
return pos
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = ops
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let warm_path = &ops[..handler_start];
let mentions_name = |unit: &CodeUnit, name: &str| match unit.op {
Instruction::LoadFast { var_num }
| Instruction::LoadFastBorrow { var_num }
| Instruction::StoreFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
};
let else_store = warm_path
.iter()
.position(|unit| {
matches!(unit.op, Instruction::StoreFast { .. }) && mentions_name(unit, "pos")
})
.expect("missing try-else pos store");
let delete_subscr = warm_path
.iter()
.position(|unit| matches!(unit.op, Instruction::DeleteSubscr))
.expect("missing join delete");
let else_tail = &warm_path[else_store + 1..delete_subscr];
assert!(
else_tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })
&& mentions_name(unit, "pos")),
"terminal try-else conditional should use strong pos loads, got tail={else_tail:?}"
);
let join_tail = &warm_path[delete_subscr.saturating_sub(6)..];
for name in ["self", "pos"] {
assert!(
join_tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })
&& mentions_name(unit, name)),
"outer if/else join should keep borrowed {name} loads, got tail={join_tail:?}"
);
}
assert!(
!join_tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })
&& (mentions_name(unit, "self") || mentions_name(unit, "pos"))),
"outer if/else join should not inherit terminal try-else deopts, got tail={join_tail:?}"
);
}
#[test]
fn terminal_except_else_final_store_attr_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(self, E, Event):
try:
decoded = bytes(self.buf).decode(self.encoding)
except E:
return
else:
self.insert(Event('key', decoded, self.flush_buf()))
self.keymap = self.compiled_keymap
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let store_idx = instructions
.iter()
.position(|unit| match unit.op {
Instruction::StoreAttr { namei } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.names[usize::try_from(namei.get(arg)).unwrap()].as_str() == "keymap"
}
_ => false,
})
.expect("missing keymap STORE_ATTR");
let insert_attr_idx = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "insert"
}
_ => false,
})
.expect("missing insert LOAD_ATTR");
assert!(
matches!(
instructions[insert_attr_idx - 1].op,
Instruction::LoadFastBorrow { .. }
),
"terminal except should not deopt the try/else method receiver before the final tail, got instructions={instructions:?}"
);
assert!(
matches!(
(
instructions[store_idx - 3].op,
instructions[store_idx - 1].op
),
(Instruction::LoadFast { .. }, Instruction::LoadFast { .. })
),
"terminal except final STORE_ATTR tail should use CPython-style strong LOAD_FAST ops, got instructions={instructions:?}"
);
}
#[test]
fn except_break_try_else_loop_tail_keeps_else_borrows() {
let code = compile_exec(
"\
def f(self):
self.setup()
prompt = 'Hit Return for more, or q (and Return) to quit: '
lineno = 0
while 1:
try:
for i in range(lineno, lineno + self.MAXLINES):
print(self.lines[i])
except IndexError:
break
else:
lineno += self.MAXLINES
key = None
while key is None:
key = input(prompt)
if key not in ('', 'q'):
key = None
if key == 'q':
break
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let borrowed_loads = instructions
.iter()
.filter(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
})
.count();
let strong_loads_in_else_tail = instructions.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFast { .. } | Instruction::LoadFastLoadFast { .. }
)
});
assert!(
borrowed_loads >= 7,
"except-break try/else loop tail should keep CPython-style borrowed loads, got instructions={instructions:?}"
);
assert!(
!strong_loads_in_else_tail,
"except-break try/else loop tail must not be deoptimized as terminal return/raise, got instructions={instructions:?}"
);
}
#[test]
fn protected_method_call_after_terminal_except_tail_uses_strong_loads() {
let code = compile_exec(
"\
def f(items, chunk, out, packI, Error):
for i in items:
acc = 0
try:
for c in chunk:
acc = acc * 85 + c
except TypeError:
raise
try:
out.append(packI(acc))
except Error:
raise ValueError from None
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let append_attr = instructions[..handler_start]
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "append"
}
_ => false,
})
.expect("missing append LOAD_ATTR");
let tail = &instructions[append_attr.saturating_sub(1)..handler_start];
let strong_loads_name = |name: &str| {
tail.iter().any(|unit| match unit.op {
Instruction::LoadFast { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
})
};
let borrows_name = |name: &str| {
tail.iter().any(|unit| match unit.op {
Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
f.varnames[usize::from(var_num.get(arg))] == name
}
_ => false,
})
};
for name in ["out", "packI", "acc"] {
assert!(
strong_loads_name(name),
"protected method-call after terminal except tail should use strong LOAD_FAST for {name}, got tail={tail:?}"
);
assert!(
!borrows_name(name),
"protected method-call after terminal except tail should not borrow {name}, got tail={tail:?}"
);
}
}
#[test]
fn terminal_reraising_handler_keeps_try_body_method_borrows() {
let code = compile_exec(
"\
def f(self):
try:
self.console.prepare()
self.arg = None
self.finished = False
del self.buffer[:]
self.pos = 0
self.dirty = True
self.last_command = None
self.calc_screen()
except BaseException:
self.restore()
raise
while self.scheduled_commands:
cmd = self.scheduled_commands.pop()
self.do_cmd((cmd, []))
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let first_handler = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let try_body = &instructions[..first_handler];
let self_borrows = try_body
.iter()
.filter(|unit| {
matches!(unit.op, Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))))]
== "self")
})
.count();
assert!(
self_borrows >= 8,
"terminal reraising handler should keep try-body self loads borrowed, got try_body={try_body:?}"
);
}
#[test]
fn terminal_except_loop_successor_augassign_uses_strong_load_pair() {
let code = compile_exec(
"\
def f(items, decoded, b32rev):
for i in range(0, len(items), 8):
quanta = items[i:i + 8]
acc = 0
try:
for c in quanta:
acc = (acc << 5) + b32rev[c]
except KeyError:
raise ValueError from None
decoded += acc.to_bytes(5)
return decoded
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let to_bytes_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "to_bytes"
}
_ => false,
})
.expect("missing to_bytes LOAD_ATTR");
let pair = instructions[to_bytes_attr.saturating_sub(1)].op;
assert!(
matches!(pair, Instruction::LoadFastLoadFast { .. }),
"terminal-except loop successor augassign should use strong LOAD_FAST_LOAD_FAST, got instructions={instructions:?}"
);
}
#[test]
fn terminal_except_loop_backedge_keeps_header_borrows() {
let code = compile_exec(
"\
def f(self, value, start=0, stop=None):
i = start
while stop is None or i < stop:
try:
v = self[i]
except IndexError:
break
if v is value or v == value:
return i
i += 1
raise ValueError
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
assert!(
instructions.windows(7).any(|window| {
matches!(window[0].op, Instruction::StoreFast { .. })
&& matches!(window[1].op, Instruction::LoadFastBorrow { .. })
&& matches!(window[2].op, Instruction::PopJumpIfNone { .. })
&& matches!(window[3].op, Instruction::NotTaken)
&& matches!(
window[4].op,
Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
&& matches!(window[5].op, Instruction::CompareOp { .. })
&& matches!(window[6].op, Instruction::PopJumpIfFalse { .. })
}),
"terminal-except loop backedge deopt should not cross into the loop header, got instructions={instructions:?}"
);
}
#[test]
fn one_line_protected_infinite_while_body_uses_strong_pair() {
let code = compile_exec(
"\
def f():
items = range(1, 4)
try:
i = 0
while 1: i = items[i]
except IndexError:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
assert!(
instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastLoadFast { .. })),
"one-line protected infinite while body should match CPython strong pair, got instructions={instructions:?}"
);
assert!(
!instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastBorrowLoadFastBorrow { .. })),
"one-line protected infinite while body should not borrow the loop body pair, got instructions={instructions:?}"
);
}
#[test]
fn multiline_protected_infinite_while_body_keeps_borrow_pair() {
let code = compile_exec(
"\
def f(items):
try:
i = 0
while 1:
i = items[i]
except IndexError:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
assert!(
instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastBorrowLoadFastBorrow { .. })),
"multiline protected infinite while body should keep CPython borrowed pair, got instructions={instructions:?}"
);
}
#[test]
fn one_line_protected_infinite_while_method_call_keeps_borrow_receiver() {
let code = compile_exec(
"\
def f(self):
try:
while 1: self.x()
except IndexError:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
assert!(
instructions
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"CPython 3.14 keeps the one-line protected bound-method receiver strong, got instructions={instructions:?}"
);
}
#[test]
fn loop_if_implicit_continue_places_body_after_jumpback() {
let code = compile_exec(
"\
def f(_config_vars, _INITPRE):
for k in list(_config_vars):
if k.startswith(_INITPRE):
del _config_vars[k]
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::DeleteSubscr,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::EndFor,
]
)
}),
"loop if with implicit continue should use CPython body-after-jumpback layout, got ops={ops:?}"
);
}
#[test]
fn loop_if_call_body_implicit_continue_places_body_after_jumpback() {
let code = compile_exec(
"\
def f(seq, db):
count = 0
for c in seq:
dec = db.value(c, -1)
if dec != -1:
db.check(dec, c)
count += 1
return count
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(8).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::Call { .. },
]
)
}),
"no-else loop call body should use CPython body-after-jumpback layout, got ops={ops:?}"
);
}
#[test]
fn nested_loop_if_try_body_implicit_continue_places_body_after_jumpback() {
let code = compile_exec(
"\
def f(seq, broken, codecs, LookupError, s, Queue, bytes):
for encoding in seq:
if encoding not in broken:
q = Queue(b'')
writer = codecs.getwriter(encoding)(q)
encodedresult = b''
for c in s:
writer.write(c)
chunk = q.read()
encodedresult += chunk
q = Queue(b'')
reader = codecs.getreader(encoding)(q)
decodedresult = ''
for c in encodedresult:
q.write(bytes([c]))
decodedresult += reader.read()
if encoding not in broken:
try:
encoder = codecs.getincrementalencoder(encoding)()
except LookupError:
pass
else:
encoder.encode('x')
if encoding not in ('idna', 'mbcs'):
try:
encoder = codecs.getincrementalencoder(encoding)('ignore')
except LookupError:
pass
else:
encodedresult = b''.join(encoder.encode(c) for c in s)
decoder = codecs.getincrementaldecoder(encoding)('ignore')
decodedresult = ''.join(decoder.decode(bytes([c])) for c in encodedresult)
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(8).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::Nop,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
]
)
}),
"nested final if with try body should put false backedge before body, got ops={ops:?}"
);
assert!(
!ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::Nop,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadAttr { .. },
]
)
}),
"nested final if with try body should not leave body before false backedge, got ops={ops:?}"
);
}
#[test]
fn loop_branch_raise_before_elif_keeps_body_before_backedge() {
let code = compile_exec(
"\
def f(checks, UNIQUE, CONTINUOUS, ValueError):
for check in checks:
if check is UNIQUE:
duplicates = []
for name in checks:
if name:
duplicates.append(name)
if duplicates:
detail = ', '.join(str(name) for name in duplicates)
raise ValueError('aliases: %s' % detail)
elif check is CONTINUOUS:
value = 1
return value
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadConst { .. },
Instruction::LoadAttr { .. },
]
)
}),
"raise body before an elif chain should stay before the branch backedge, got ops={ops:?}"
);
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadConst { .. },
]
)
}),
"raise body before an elif chain should not be moved after the branch backedge, got ops={ops:?}"
);
}
#[test]
fn loop_nested_raise_then_append_places_body_after_false_backedge() {
let code = compile_exec(
"\
def f(args, parameters, enforce_default_ordering, type_var_tuple_encountered, default_encountered, TypeError):
for t in args:
if t not in parameters:
if enforce_default_ordering:
if type_var_tuple_encountered and t.has_default():
raise TypeError('a')
if t.has_default():
default_encountered = True
elif default_encountered:
raise TypeError('b')
parameters.append(t)
return parameters
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
]
)
}),
"CPython places the nested raise/append body after the false backedge for this loop tail, got ops={ops:?}"
);
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::ToBool,
]
)
}),
"this case should not keep the body before the false backedge; CPython emits the split backedge first, got ops={ops:?}"
);
}
#[test]
fn loop_break_before_adjacent_break_keeps_body_before_backedge() {
let code = compile_exec(
"\
def f(pattern, prefix, get_prefix):
for op, av in pattern:
if op == 1:
prefix.append(av)
elif op == 2:
prefix1, got_all = get_prefix(av)
prefix.extend(prefix1)
if not got_all:
break
else:
break
else:
return prefix, True
return prefix, False
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::PopTop,
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::LoadConst { .. },
Instruction::BuildTuple { .. },
]
)
}),
"break before an adjacent break exit should stay before the loop backedge, got ops={ops:?}"
);
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::PopTop,
]
)
}),
"break before an adjacent break exit should not be moved after the loop backedge, got ops={ops:?}"
);
}
#[test]
fn loop_elif_and_pass_keeps_shared_false_backedge_after_body() {
let code = compile_exec(
"\
def f(methods, simple_keys, checked_keys, checked_enum, simple_enum, failed):
for method in methods:
if method in simple_keys and method in checked_keys:
continue
elif method not in simple_keys and method not in checked_keys:
checked_method = getattr(checked_enum, method, None)
simple_method = getattr(simple_enum, method, None)
if hasattr(checked_method, '__func__'):
checked_method = checked_method.__func__
simple_method = simple_method.__func__
if checked_method != simple_method:
failed.append(method)
else:
pass
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(9).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::ContainsOp { .. },
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadGlobal { .. },
_,
]
)
}),
"elif-and body should stay before the shared false backedge, got ops={ops:?}"
);
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::ContainsOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
]
)
}),
"elif-and shared false backedge should not be split before the body, got ops={ops:?}"
);
}
#[test]
fn loop_nested_if_delete_slice_places_body_after_jumpback() {
let code = compile_exec(
"\
def f(compiler_so):
for idx in reversed(range(len(compiler_so))):
if compiler_so[idx] == '-arch' and compiler_so[idx + 1] == 'arm64':
del compiler_so[idx:idx + 2]
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(15).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::LoadSmallInt { .. },
Instruction::BinaryOp { .. },
Instruction::BinaryOp { .. },
Instruction::LoadConst { .. },
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
]
)
}),
"nested loop delete-slice condition should put false jump-back before body, got ops={ops:?}"
);
}
#[test]
fn loop_if_subscr_store_delete_places_body_after_jumpback() {
let code = compile_exec(
"\
def f(chunks):
for k in range(len(chunks)-1, 0, -1):
if chunks[k-1][-1] > chunks[k][0]:
chunks[k-1] = chunks[k-1][:-1] + chunks[k][1:]
del chunks[k]
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::CompareOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::LoadSmallInt { .. },
Instruction::BinaryOp { .. },
]
)
}),
"loop if with subscript store/delete body should put false jump-back before body, got ops={ops:?}"
);
}
#[test]
fn final_elif_implicit_continue_places_jumpback_before_body() {
let code = compile_exec(
"\
def f(state, nextchar, whitespace, token, posix, quoted, debug):
while True:
if state is None:
break
elif state == ' ':
if not nextchar:
state = None
break
elif nextchar in whitespace:
if debug >= 2:
print('x')
if token or (posix and quoted):
break
else:
continue
elif state in ('a', 'c'):
if not nextchar:
state = None
break
elif nextchar in whitespace:
if debug >= 2:
print('y')
state = ' '
if token or (posix and quoted):
break
else:
continue
return token
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::ContainsOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"final elif with implicit continue should put false jump-back before body, got ops={ops:?}"
);
}
#[test]
fn final_attribute_elif_implicit_continue_places_jumpback_before_body() {
let code = compile_exec(
"\
def f(self, nextchar, quoted):
while True:
if self.state is None:
break
elif self.state in ('a', 'c'):
if not nextchar:
self.state = None
break
elif nextchar in self.whitespace:
self.state = ' '
if self.token or (self.posix and quoted):
break
else:
continue
return self.token
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(6).any(|window| {
matches!(
window,
[
Instruction::LoadFastBorrowLoadFastBorrow { .. }
| Instruction::LoadFastLoadFast { .. },
Instruction::LoadAttr { .. },
Instruction::ContainsOp { .. },
Instruction::PopJumpIfTrue { .. },
Instruction::NotTaken,
Instruction::JumpBackward { .. }
| Instruction::JumpBackwardNoInterrupt { .. },
]
)
}),
"final attribute elif with implicit continue should put false jump-back before body, got ops={ops:?}"
);
}
#[test]
fn inner_if_implicit_continue_keeps_line_bearing_body_before_backedge() {
let code = compile_exec(
"\
def f(self, nextchar, quoted):
while True:
if self.state is None:
break
elif self.state in ('a', 'c'):
if not nextchar:
self.state = None
break
elif nextchar in self.whitespace:
self.state = ' '
if self.token or (self.posix and quoted):
break
else:
continue
elif nextchar in self.commenters:
self.instream.readline()
self.lineno += 1
if self.posix:
self.state = ' '
if self.token or (self.posix and quoted):
break
else:
continue
elif self.state == 'c':
break
return self.token
",
);
let f = find_code(&code, "f").expect("missing function code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
ops.windows(7).any(|window| {
matches!(
window,
[
Instruction::LoadAttr { .. },
Instruction::ToBool,
Instruction::PopJumpIfFalse { .. },
Instruction::NotTaken,
Instruction::LoadConst { .. },
Instruction::LoadFastBorrow { .. } | Instruction::LoadFast { .. },
Instruction::StoreAttr { .. },
]
)
}),
"line-bearing inner if with implicit continue should keep body before backedge, got ops={ops:?}"
);
}
#[test]
fn except_handler_with_conditional_raise_and_resume_keeps_borrow() {
let code = compile_exec(
"\
def f(formatstr, args, output, overflowok):
try:
result = formatstr % args
except OverflowError:
if not overflowok:
raise
print('overflow')
else:
if output and result != output:
raise AssertionError(result, output)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let assertion_error = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadGlobal { namei } => {
let load_global = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_global >> 1).unwrap()].as_str() == "AssertionError"
}
_ => false,
})
.expect("missing AssertionError raise");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let tail = &instructions[..handler_start.min(assertion_error)];
assert!(
tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"conditional-raise handler with a resume path should keep borrowed loads, got tail={tail:?}"
);
assert!(
tail.iter()
.all(|unit| !matches!(unit.op, Instruction::LoadFast { .. })),
"conditional-raise handler with a resume path should not force strong LOAD_FAST, got tail={tail:?}"
);
}
#[test]
fn typed_except_resume_import_warning_tail_keeps_borrows() {
let code = compile_exec(
r#"
def f(mod_name, error, sys, RuntimeWarning):
pkg_name, _, _ = mod_name.rpartition(".")
if pkg_name:
try:
__import__(pkg_name)
except ImportError as e:
if e.name is None or (e.name != pkg_name and
not pkg_name.startswith(e.name + ".")):
raise
existing = sys.modules.get(mod_name)
if existing is not None and not hasattr(existing, "__path__"):
from warnings import warn
msg = "{mod_name!r} found in sys.modules after import of " \
"package {pkg_name!r}, but prior to execution of " \
"{mod_name!r}; this may result in unpredictable " \
"behaviour".format(mod_name=mod_name, pkg_name=pkg_name)
warn(RuntimeWarning(msg))
return mod_name
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let arg = |unit: &bytecode::CodeUnit| OpArg::new(u32::from(u8::from(unit.arg)));
let strong_names = |unit: &bytecode::CodeUnit| -> Vec<&str> {
match unit.op {
Instruction::LoadFast { var_num } => {
vec![f.varnames[usize::from(var_num.get(arg(unit)))].as_str()]
}
Instruction::LoadFastLoadFast { var_nums } => {
let (left, right) = var_nums.get(arg(unit)).indexes();
vec![
f.varnames[usize::from(left)].as_str(),
f.varnames[usize::from(right)].as_str(),
]
}
_ => Vec::new(),
}
};
let borrowed_names = |unit: &bytecode::CodeUnit| -> Vec<&str> {
match unit.op {
Instruction::LoadFastBorrow { var_num } => {
vec![f.varnames[usize::from(var_num.get(arg(unit)))].as_str()]
}
Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
let (left, right) = var_nums.get(arg(unit)).indexes();
vec![
f.varnames[usize::from(left)].as_str(),
f.varnames[usize::from(right)].as_str(),
]
}
_ => Vec::new(),
}
};
let handler_start = f
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let normal_path = &f.instructions[..handler_start];
for name in ["mod_name", "existing", "warn", "msg"] {
assert!(
normal_path
.iter()
.flat_map(borrowed_names)
.any(|borrowed| borrowed == name),
"CPython keeps {name} borrowed after typed-except resume, got ops={:?}",
normal_path.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
assert!(
!normal_path
.iter()
.flat_map(strong_names)
.any(|strong| strong == name),
"typed-except resume should not force strong {name} loads, got ops={:?}",
normal_path.iter().map(|unit| unit.op).collect::<Vec<_>>()
);
}
}
#[test]
fn reraising_except_else_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self, data, length):
if self._paused:
self._pending_data_length = length
return
if length == 0:
self._eof_received()
return
if isinstance(self._protocol, protocols.BufferedProtocol):
try:
protocols._feed_data_to_buffered_proto(self._protocol, data)
except (SystemExit, KeyboardInterrupt):
raise
except BaseException as exc:
self._fatal_error(exc, 'x')
return
else:
self._protocol.data_received(data)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let data_received_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== "data_received"
}
_ => false,
})
.expect("missing data_received LOAD_ATTR");
let ops: Vec<_> = instructions.iter().map(|unit| unit.op).collect();
assert!(
matches!(
ops.get(data_received_attr - 2),
Some(Instruction::LoadFastBorrow { .. })
),
"normal else tail after reraising handler should keep borrowed self load, got ops={ops:?}"
);
assert!(
matches!(
ops.get(data_received_attr + 1),
Some(Instruction::LoadFastBorrow { .. })
),
"normal else tail after reraising handler should keep borrowed data load, got ops={ops:?}"
);
}
#[test]
fn try_else_finally_with_keeps_context_manager_borrow() {
let code = compile_exec(
"\
def f(i):
try:
1 / 0
except ZeroDivisionError:
pass
else:
with i as dodgy:
pass
finally:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let first_with_exit = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadSpecial { method } => {
method.get(OpArg::new(u32::from(u8::from(unit.arg)))) == SpecialMethod::Exit
}
_ => false,
})
.expect("missing __exit__ load");
assert!(
matches!(
instructions
.get(first_with_exit.saturating_sub(2))
.map(|unit| unit.op),
Some(Instruction::LoadFastBorrow { .. })
),
"try/except/else/finally with setup should keep CPython-style borrowed context manager load, got instructions={instructions:?}"
);
}
#[test]
fn except_star_handler_pop_block_does_not_leave_nop_before_with_exit() {
let code = compile_exec(
"\
def f(self):
with self.assertRaises(TypeError):
try:
raise OSError('blah')
except* ExceptionGroup as e:
pass
",
);
let f = find_code(&code, "f").expect("missing f code");
let ops: Vec<_> = f
.instructions
.iter()
.map(|unit| unit.op)
.filter(|op| !matches!(op, Instruction::Cache))
.collect();
assert!(
!ops.windows(5).any(|window| {
matches!(
window,
[
Instruction::Reraise { .. },
Instruction::Nop,
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
Instruction::LoadConst { .. },
]
)
}),
"except* handler cleanup should not leave an extra NOP before with-exit None loads, got ops={ops:?}"
);
}
#[test]
fn except_star_body_to_else_jump_drops_without_line_nop() {
let code = compile_exec(
"\
async def f(self, cm):
try:
async with cm:
pass
except* Exception:
pass
else:
self.fail()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let fail_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "fail"
}
_ => false,
})
.expect("missing self.fail load");
assert!(
!matches!(
instructions
.get(fail_attr.saturating_sub(2))
.map(|unit| unit.op),
Some(Instruction::Nop)
),
"body-to-else jump should be no-location and disappear, got instructions={instructions:?}"
);
}
#[test]
fn resuming_except_before_with_keeps_with_body_borrows() {
let code = compile_exec(
"\
def f(self, cm):
try:
g()
except OSError:
pass
with cm:
self.x()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let first_tail_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "x"
}
_ => false,
})
.expect("missing x attr load");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let with_tail = &instructions[first_tail_attr.saturating_sub(1)..handler_start];
assert!(
with_tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"resuming except before with should keep borrowed LOAD_FAST ops, got tail={with_tail:?}"
);
}
#[test]
fn nested_finally_except_resume_loop_uses_strong_loads() {
let code = compile_exec(
"\
def f(self, xs):
try:
try:
g()
finally:
h()
except OSError:
self.skipTest('x')
for x in xs:
self.x(x)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let for_iter = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::ForIter { .. }))
.expect("missing FOR_ITER");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let loop_tail = &instructions[for_iter.saturating_sub(2)..handler_start];
assert!(
loop_tail
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"expected nested finally/except resume loop to use strong LOAD_FAST ops, got tail={loop_tail:?}"
);
assert!(
loop_tail.iter().all(|unit| {
!matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"nested finally/except resume loop should not borrow LOAD_FAST ops, got tail={loop_tail:?}"
);
}
#[test]
fn finally_protected_loop_without_except_resume_keeps_borrows() {
let code = compile_exec(
"\
def f(self, obj, expected, buf):
try:
lines = obj.readlines()
except ValueError:
self.fail('x')
if lines != expected:
self.fail('bad')
obj.close()
obj = self.open()
try:
for line in obj:
pass
try:
obj.readline()
obj.readinto(buf)
except ValueError:
self.fail('inner')
finally:
obj.close()
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let get_iter = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::GetIter))
.expect("missing GET_ITER");
assert!(
matches!(
instructions
.get(get_iter.saturating_sub(1))
.map(|unit| unit.op),
Some(Instruction::LoadFastBorrow { .. })
),
"finally-protected loop without except resume should keep borrowed iterable load, got instructions={instructions:?}"
);
for attr_name in ["close", "open"] {
let attr_idx = instructions[..get_iter]
.iter()
.rposition(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str()
== attr_name
}
_ => false,
})
.unwrap_or_else(|| panic!("missing {attr_name} attr load before loop"));
assert!(
matches!(
instructions
.get(attr_idx.saturating_sub(1))
.map(|unit| unit.op),
Some(Instruction::LoadFastBorrow { .. })
),
"pre-loop {attr_name} call should keep borrowed receiver load, got instructions={instructions:?}"
);
}
}
#[test]
fn plain_except_resume_loop_keeps_borrows() {
let code = compile_exec(
"\
def f(self, xs):
try:
g()
except OSError:
self.skipTest('x')
for x in xs:
self.x(x)
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let for_iter = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::ForIter { .. }))
.expect("missing FOR_ITER");
let handler_start = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::PushExcInfo))
.expect("missing handler entry");
let loop_tail = &instructions[for_iter.saturating_sub(2)..handler_start];
assert!(
loop_tail.iter().any(|unit| {
matches!(
unit.op,
Instruction::LoadFastBorrow { .. }
| Instruction::LoadFastBorrowLoadFastBorrow { .. }
)
}),
"plain except resume loop should keep borrowed LOAD_FAST ops, got tail={loop_tail:?}"
);
}
#[test]
fn except_pass_resume_loop_branch_keeps_borrows() {
let code = compile_exec(
r#"
def f(self, cls, fns):
for name, fn in zip(self.names, fns):
try:
annotation_fields, return_type = self.method_annotations[name]
except KeyError:
pass
else:
annotate_fn = _make_annotate_function(cls, name, annotation_fields, return_type)
fn.__annotate__ = annotate_fn
if self.unconditional_adds.get(name, False):
setattr(cls, name, fn)
else:
already_exists = _set_new_attribute(cls, name, fn)
if already_exists and (msg_extra := self.overwrite_errors.get(name)):
error_msg = (f'Cannot overwrite attribute {fn.__name__} '
f'in class {cls.__name__}')
if not msg_extra is True:
error_msg = f'{error_msg} {msg_extra}'
raise TypeError(error_msg)
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let load_global_name = |unit: &&CodeUnit, name: &str| match unit.op {
Instruction::LoadGlobal { namei } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let load_global = namei.get(arg) >> 1;
f.names[usize::try_from(load_global).unwrap()].as_str() == name
}
_ => false,
};
let load_name = |unit: &&CodeUnit| match unit.op {
Instruction::LoadFast { var_num } | Instruction::LoadFastBorrow { var_num } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
Some(f.varnames[usize::from(var_num.get(arg))].as_str())
}
_ => None,
};
let load_pair_names = |unit: &&CodeUnit| match unit.op {
Instruction::LoadFastLoadFast { var_nums }
| Instruction::LoadFastBorrowLoadFastBorrow { var_nums } => {
let arg = OpArg::new(u32::from(u8::from(unit.arg)));
let (left, right) = var_nums.get(arg).indexes();
Some((
f.varnames[usize::from(left)].as_str(),
f.varnames[usize::from(right)].as_str(),
))
}
_ => None,
};
let set_new_attribute = instructions
.iter()
.position(|unit| load_global_name(unit, "_set_new_attribute"))
.expect("missing _set_new_attribute call");
let tail = &instructions[set_new_attribute..instructions.len()];
assert!(
tail.iter().any(|unit| {
matches!(unit.op, Instruction::LoadFastBorrowLoadFastBorrow { .. })
&& load_pair_names(unit) == Some(("cls", "name"))
}) && tail.iter().any(|unit| {
matches!(unit.op, Instruction::LoadFastBorrow { .. })
&& load_name(unit) == Some("fn")
}),
"except-pass resume should keep CPython-style borrowed loads in loop branch, got tail={tail:?}"
);
assert!(
!tail.iter().any(|unit| {
matches!(unit.op, Instruction::LoadFastLoadFast { .. })
&& load_pair_names(unit) == Some(("cls", "name"))
}) && !tail.iter().any(|unit| {
matches!(unit.op, Instruction::LoadFast { .. })
&& matches!(
load_name(unit),
Some("fn" | "already_exists" | "msg_extra" | "error_msg")
)
}),
"except-pass resume must not deopt the independent loop branch, got tail={tail:?}"
);
}
#[test]
fn named_except_cleanup_deopts_same_guard_fallbacks_not_outer_tail() {
let code = compile_exec(
r#"
def f(s, size, errors, final):
found_invalid_escape = False
p = []
pos = 0
while pos < size:
ch = chr(s[pos])
pos += 1
if ch == "N":
message = "malformed \\N character escape"
look = pos
try:
import unicodedata
except ImportError:
message = "\\N escapes not supported (can't load unicodedata module)"
unicode_call_errorhandler(
errors, "unicodeescape", message, s, pos - 1, size
)
continue
if look < size and chr(s[look]) == "{":
while look < size and chr(s[look]) != "}":
look += 1
if look > pos + 1 and look < size and chr(s[look]) == "}":
message = "unknown Unicode character name"
st = s[pos + 1 : look]
try:
chr_codec = unicodedata.lookup("%s" % st)
except LookupError as e:
x = unicode_call_errorhandler(
errors, "unicodeescape", message, s, pos - 1, look + 1
)
else:
x = chr_codec, look + 1
p.append(x[0])
pos = x[1]
else:
if not final:
pos = 0
break
x = unicode_call_errorhandler(
errors, "unicodeescape", message, s, pos - 1, look + 1
)
p.append(x[0])
pos = x[1]
else:
if not final:
pos = 0
break
x = unicode_call_errorhandler(
errors, "unicodeescape", message, s, pos - 1, look + 1
)
p.append(x[0])
pos = x[1]
else:
if not found_invalid_escape:
found_invalid_escape = True
warnings.warn(
"invalid escape sequence '\\%c'" % ch, DeprecationWarning, 2
)
p.append("\\")
p.append(ch)
return p, pos
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let mut saw_strong_final = false;
let mut saw_borrow_p_after_warn = false;
let mut saw_borrow_ch_after_warn = false;
let mut after_warn_attr = false;
for unit in f.instructions.iter() {
match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
let name = f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str();
if name == "warn" {
after_warn_attr = true;
}
}
Instruction::LoadFast { var_num } => {
let idx = usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))));
let name = f.varnames[idx].as_str();
if name == "final" {
saw_strong_final = true;
}
}
Instruction::LoadFastBorrow { var_num } => {
let idx = usize::from(var_num.get(OpArg::new(u32::from(u8::from(unit.arg)))));
let name = f.varnames[idx].as_str();
if after_warn_attr && name == "p" {
saw_borrow_p_after_warn = true;
}
if after_warn_attr && name == "ch" {
saw_borrow_ch_after_warn = true;
}
}
_ => {}
}
}
assert!(
saw_strong_final,
"expected named-except fallback guards to deopt final to strong LOAD_FAST"
);
assert!(
saw_borrow_p_after_warn && saw_borrow_ch_after_warn,
"expected outer invalid-escape tail to keep borrowed p/ch loads"
);
}
#[test]
fn imap_idle_status_debug_tail_keeps_borrow() {
let code = compile_exec(
"\
def f(self, exc_type, CRLF, OSError):
imap = self._imap
try:
imap.send(b'DONE' + CRLF)
status, [msg] = imap._command_complete('IDLE', self._tag)
if __debug__ and imap.debug >= 4:
imap._mesg(f'idle status: {status} {msg!r}')
except OSError:
if not exc_type:
raise
return False
",
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let mesg_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "_mesg"
}
_ => false,
})
.expect("missing _mesg attr load");
let tail = &instructions[mesg_attr.saturating_sub(1)..];
assert!(
tail.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })),
"expected idle status debug tail to keep borrowed loads, got tail={tail:?}"
);
}
#[test]
fn match_async_comprehension_iter_keeps_capture_borrow() {
let code = compile_exec(
r#"
async def name_4():
match b'':
case True:
pass
case name_5 if f'e':
{name_3: name_4 async for name_2 in name_5}
case []:
pass
[[]]
"#,
);
let name_4 = find_code(&code, "name_4").expect("missing name_4 code");
let Some(get_aiter_pos) = name_4
.instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::GetAiter))
else {
panic!("missing GET_AITER in name_4");
};
let prev = &name_4.instructions[get_aiter_pos - 1];
assert!(
matches!(
prev.op,
Instruction::LoadFastBorrow { var_num }
if name_4.varnames[usize::from(var_num.get(OpArg::new(u32::from(u8::from(prev.arg)))))] == "name_5"
),
"expected async comprehension iterator capture to borrow name_5 before GET_AITER, got {prev:?}"
);
}
#[test]
fn match_fail_cleanup_label_reuse_keeps_post_match_borrow() {
let code = compile_exec(
r#"
def f(self):
match 0:
case 0:
x = True
self.assertIs(x, True)
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let assert_is_attr = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadAttr { namei } => {
let load_attr = namei.get(OpArg::new(u32::from(u8::from(unit.arg))));
f.names[usize::try_from(load_attr.name_idx()).unwrap()].as_str() == "assertIs"
}
_ => false,
})
.expect("missing assertIs attr load");
let receiver = &instructions[assert_is_attr - 1];
assert!(
matches!(
receiver.op,
Instruction::LoadFastBorrow { var_num }
if f.varnames[usize::from(var_num.get(OpArg::new(u32::from(u8::from(receiver.arg)))))] == "self"
),
"CPython codegen_match_inner uses USE_LABEL(c, end), so post-match receiver should stay borrowed; got {receiver:?}"
);
}
#[test]
fn match_or_preserves_explicit_success_jumps() {
let code = compile_exec(
r#"
def f(w):
match w:
case 1 | 2 | 3:
out = locals()
del out["w"]
return out
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let locals_pos = instructions
.iter()
.position(|unit| match unit.op {
Instruction::LoadGlobal { namei } => {
let name = namei.get(OpArg::new(u32::from(u8::from(unit.arg)))) >> 1;
f.names[usize::try_from(name).unwrap()].as_str() == "locals"
}
_ => false,
})
.expect("missing locals load");
let pattern_prefix = &instructions[..locals_pos];
let false_jumps = pattern_prefix
.iter()
.filter(|unit| matches!(unit.op, Instruction::PopJumpIfFalse { .. }))
.count();
let success_jumps = pattern_prefix
.iter()
.filter(|unit| matches!(unit.op, Instruction::JumpForward { .. }))
.count();
assert_eq!(
false_jumps, 3,
"CPython codegen_pattern_or() keeps each alternative as false-jump plus success jump; got prefix={pattern_prefix:?}"
);
assert_eq!(
success_jumps, 3,
"CPython codegen_pattern_or() keeps explicit success JUMPs for all alternatives; got prefix={pattern_prefix:?}"
);
}
#[test]
fn with_protected_generator_tail_after_cleanup_uses_strong_loads() {
let code = compile_exec(
r#"
def f(scandir, fspath, path, reversed, top, OSError, topdown=True, followlinks=False):
stack = [fspath(top)]
islink, join = path.islink, path.join
while stack:
top = stack.pop()
dirs = []
nondirs = []
walk_dirs = []
try:
with scandir(top) as entries:
for entry in entries:
try:
is_dir = entry.is_dir()
except OSError:
is_dir = False
if is_dir:
dirs.append(entry.name)
else:
nondirs.append(entry.name)
if not topdown and is_dir:
if followlinks:
walk_into = True
else:
try:
is_symlink = entry.is_symlink()
except OSError:
is_symlink = False
walk_into = not is_symlink
if walk_into:
walk_dirs.append(entry.path)
except OSError:
continue
if topdown:
yield top, dirs, nondirs
for dirname in reversed(dirs):
new_path = join(top, dirname)
if not followlinks and islink(new_path):
continue
stack.append(new_path)
else:
stack.append((top, dirs, nondirs))
for new_path in reversed(walk_dirs):
stack.append(new_path)
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let yield_pos = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::YieldValue { .. }))
.expect("missing YIELD_VALUE");
assert!(
matches!(
instructions[yield_pos - 3].op,
Instruction::LoadFastLoadFast { .. }
) && matches!(instructions[yield_pos - 2].op, Instruction::LoadFast { .. }),
"CPython keeps the yielded tuple inputs strong after with cleanup, got {:?} {:?}",
instructions[yield_pos - 3],
instructions[yield_pos - 2],
);
assert!(
instructions[yield_pos + 1..]
.iter()
.take(30)
.any(|unit| matches!(unit.op, Instruction::LoadFast { .. })),
"expected post-yield traversal tail to contain strong LOAD_FAST ops"
);
}
#[test]
fn yield_from_finally_cleanup_keeps_normal_path_borrows() {
let code = compile_exec(
r#"
def f(_fwalk, stack, isbytes, topdown, onerror, follow_symlinks, close):
try:
while stack:
yield from _fwalk(stack, isbytes, topdown, onerror, follow_symlinks)
finally:
while stack:
action, value = stack.pop()
if action == 2:
close(value)
"#,
);
let f = find_code(&code, "f").expect("missing f code");
let instructions: Vec<_> = f
.instructions
.iter()
.filter(|unit| !matches!(unit.op, Instruction::Cache))
.collect();
let send_pos = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::Send { .. }))
.expect("missing SEND");
assert!(
instructions[..send_pos]
.iter()
.any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })),
"CPython keeps yield-from normal path loads borrowed, got prefix={:?}",
&instructions[..send_pos]
);
let cleanup_pop = instructions
.iter()
.position(|unit| matches!(unit.op, Instruction::StoreFastStoreFast { .. }))
.expect("missing finally cleanup unpack");
assert!(
instructions[..cleanup_pop]
.iter()
.rev()
.take(12)
.any(|unit| matches!(unit.op, Instruction::LoadFastBorrow { .. })),
"CPython keeps normal finally cleanup loads borrowed, got cleanup prefix={:?}",
&instructions[cleanup_pop.saturating_sub(12)..cleanup_pop]
);
}
}