mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 10:44:36 +08:00
* 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
44140 lines
1.5 MiB
Plaintext
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(©, 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(¤t_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 = ¤t_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(¶meters.posonlyargs)
|
|
.chain(¶meters.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(¶meters.posonlyargs)
|
|
.chain(¶meters.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 ¶meters.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) = ¶m.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(¶ms.posonlyargs)
|
|
.chain(¶ms.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 ¶ms.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(¶ms.args)
|
|
.chain(¶ms.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(¶meters.args)
|
|
.chain(¶meters.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(¶meters.args)
|
|
.chain(¶meters.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]
|
|
);
|
|
}
|
|
}
|