diff --git a/.cspell.dict/cpython.txt b/.cspell.dict/cpython.txt index 81274276e..f0b5e608d 100644 --- a/.cspell.dict/cpython.txt +++ b/.cspell.dict/cpython.txt @@ -40,6 +40,7 @@ cidx cjkcodecs CLASSDEREF classdict +cleandoc cmpop CNOTAB codedepth @@ -61,6 +62,7 @@ dictbytype DICTFLAG dictoffset distpoint +drvsize dynload elts eooh @@ -85,6 +87,7 @@ finalizers firsttraceable flowgraph formatfloat +framesize freelist freevar freevars @@ -93,6 +96,7 @@ fsrc getdict getfunc getiter +getlasti getsets getslice globalgetvar @@ -145,6 +149,7 @@ mbiencoder mbstreamreader mbstreamwriter metavars +mimalloc miscompiles mult multibytecodec @@ -177,6 +182,9 @@ npools nseen NSIGNALS numer +numdupes +numkwonly +numposorkw nvars obmalloc opname @@ -201,6 +209,7 @@ preds preinitialized pybuilddir pycore +PYGC pyinner pydecimal pyerrors @@ -215,6 +224,7 @@ pystrcmp PYTHONTRACEMALLOC PYTHONUTF8 pythonw +pytime PYTHREAD_NAME recvd releasebuffer @@ -258,6 +268,8 @@ templatelib testconsole threadstate ticketer +timespec +timeval tmptype tok_oldval tstate diff --git a/Lib/test/test_class.py b/Lib/test/test_class.py index ec65819d2..d36c52316 100644 --- a/Lib/test/test_class.py +++ b/Lib/test/test_class.py @@ -574,7 +574,6 @@ class ClassTests(unittest.TestCase): else: self.fail("Failed to raise RecursionError") - @unittest.expectedFailure # TODO: RUSTPYTHON def testForExceptionsRaisedInInstanceGetattr2(self): # Tests for exceptions raised in instance_getattr2(). @@ -858,7 +857,7 @@ class ClassTests(unittest.TestCase): pass -# from _testinternalcapi import has_inline_values # XXX: RUSTPYTHON +from _testinternalcapi import has_inline_values Py_TPFLAGS_INLINE_VALUES = (1 << 2) Py_TPFLAGS_MANAGED_DICT = (1 << 4) @@ -886,14 +885,12 @@ class VarSizedSubclass(tuple): class TestInlineValues(unittest.TestCase): - @unittest.expectedFailure # TODO: RUSTPYTHON; NameError: name 'has_inline_values' is not defined. def test_no_flags_for_slots_class(self): flags = NoManagedDict.__flags__ self.assertEqual(flags & Py_TPFLAGS_MANAGED_DICT, 0) self.assertEqual(flags & Py_TPFLAGS_INLINE_VALUES, 0) self.assertFalse(has_inline_values(NoManagedDict())) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0 != 4 def test_both_flags_for_regular_class(self): for cls in (Plain, WithAttrs): with self.subTest(cls=cls.__name__): @@ -902,33 +899,28 @@ class TestInlineValues(unittest.TestCase): self.assertEqual(flags & Py_TPFLAGS_INLINE_VALUES, Py_TPFLAGS_INLINE_VALUES) self.assertTrue(has_inline_values(cls())) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 0 != 4 def test_managed_dict_only_for_varsized_subclass(self): flags = VarSizedSubclass.__flags__ self.assertEqual(flags & Py_TPFLAGS_MANAGED_DICT, Py_TPFLAGS_MANAGED_DICT) self.assertEqual(flags & Py_TPFLAGS_INLINE_VALUES, 0) self.assertFalse(has_inline_values(VarSizedSubclass())) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_has_inline_values(self): c = Plain() self.assertTrue(has_inline_values(c)) del c.__dict__ self.assertFalse(has_inline_values(c)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_instances(self): self.assertTrue(has_inline_values(Plain())) self.assertTrue(has_inline_values(WithAttrs())) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_inspect_dict(self): for cls in (Plain, WithAttrs): c = cls() c.__dict__ self.assertTrue(has_inline_values(c)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_update_dict(self): d = { "e": 5, "f": 6 } for cls in (Plain, WithAttrs): @@ -945,7 +937,6 @@ class TestInlineValues(unittest.TestCase): for i in range(100): self.assertEqual(getattr(obj, f"a{i}"), i) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_many_attributes(self): class C: pass c = C() @@ -956,7 +947,6 @@ class TestInlineValues(unittest.TestCase): c = C() self.assertTrue(has_inline_values(c)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_many_attributes_with_dict(self): class C: pass c = C() diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 7080d7c40..298bbfb79 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -1112,7 +1112,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): with self.assertRaises(TypeError): frozenset().__class__ = MyFrozenSet - @unittest.expectedFailure # TODO: RUSTPYTHON @support.thread_unsafe def test_slots(self): # Testing __slots__... @@ -1368,7 +1367,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): a.foo = 42 self.assertEqual(a.__dict__, {"foo": 42}) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_slots_special2(self): # Testing __qualname__ and __classcell__ in __slots__ class Meta(type): @@ -1940,7 +1938,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): self.assertEqual(b.foo, 3) self.assertEqual(b.__class__, B) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_altmro(self): # Testing mro() and overriding it... class A(object): @@ -2119,7 +2116,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): set_add.__get__(0) self.assertEqual(cm.exception.args[0], expected_errmsg) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_special_method_lookup(self): # The lookup of special methods bypasses __getattr__ and # __getattribute__, but they still can be descriptors. @@ -2680,7 +2676,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): dir(C()) # This used to segfault - @unittest.expectedFailure # TODO: RUSTPYTHON def test_supers(self): # Testing super... @@ -3349,7 +3344,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): self.assertEqual(NewClass.__doc__, 'object=None; type=NewClass') self.assertEqual(NewClass().__doc__, 'object=NewClass instance; type=NewClass') - @unittest.expectedFailure # TODO: RUSTPYTHON def test_set_class(self): # Testing __class__ assignment... class C(object): pass @@ -3824,7 +3818,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): m.foo = 1 self.assertEqual(m.__dict__, {"foo": 1}) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_funny_new(self): # Testing __new__ returning something unexpected... class C(object): @@ -4235,7 +4228,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): with self.assertRaises(TypeError): X.__bases__ = type(None), O - @unittest.expectedFailure # TODO: RUSTPYTHON def test_mutable_bases_with_failing_mro(self): # Testing mutable bases with failing mro... class WorkOnce(type): @@ -4476,7 +4468,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): self.assertIsInstance(a, C) # Baseline self.assertIsInstance(pa, C) # Test - @unittest.expectedFailure # TODO: RUSTPYTHON def test_proxy_super(self): # Testing super() for a proxy object... class Proxy(object): @@ -4500,7 +4491,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): p = Proxy(obj) self.assertEqual(C.__dict__["f"](p), "B.f->C.f") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_carloverre(self): # Testing prohibition of Carlo Verre's hack... try: @@ -4533,7 +4523,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): except TypeError: self.fail("setattr through direct base types should be legal") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_carloverre_multi_inherit_invalid(self): class A(type): def __setattr__(cls, key, value): @@ -5900,7 +5889,6 @@ class MroTest(unittest.TestCase): class C(B): pass - @unittest.expectedFailure # TODO: RUSTPYTHON def test_reent_set_bases_tp_base_cycle(self): """ type_set_bases must check for an inheritance cycle not only through @@ -6043,7 +6031,6 @@ class MroTest(unittest.TestCase): class A(metaclass=M): pass - @unittest.expectedFailure # TODO: RUSTPYTHON def test_disappearing_custom_mro(self): """ gh-92112: A custom mro() returning a result conflicting with diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 855e1c79c..3ed2aabaf 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -1205,6 +1205,7 @@ class GCTests(unittest.TestCase): assert_python_ok("-c", source) + @unittest.expectedFailure # TODO: RUSTPYTHON; long_lived_total does not count untracked tuples @unittest.skipUnless(Py_GIL_DISABLED, "requires free-threaded GC") @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") def test_tuple_untrack_counts(self): @@ -1222,6 +1223,7 @@ class GCTests(unittest.TestCase): # Use n // 2 just in case some other objects were collected. self.assertTrue(new_count - count > (n // 2)) + @unittest.expectedFailure # TODO: RUSTPYTHON; tracked heap size is not generational @requires_gil_enabled('need generational GC') @unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi") def test_heap_size(self): diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index ac9a2b1a2..9dc80d08f 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -4308,7 +4308,6 @@ class TestSignatureObject(unittest.TestCase): self.assertEqual(self.signature(C.__new__, follow_wrapped=False), varargs_signature) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_signature_on_class_with_wrapped_new(self): with self.subTest('FunctionType'): class C: diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index 30eee65dc..e59fce573 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -1054,6 +1054,7 @@ class ExceptionMonitoringTest(CheckEvents): ) self.assertEqual(events[0], ("throw", IndexError)) + @unittest.expectedFailure # TODO: RUSTPYTHON; CALL_ALLOC_AND_ENTER_INIT specialization @unittest.skipUnless(_testinternalcapi, "requires _testinternalcapi") @requires_specialization_ft def test_no_unwind_for_shim_frame(self): @@ -1558,7 +1559,6 @@ BRANCH_OFFSET_RECORDERS = BranchLeftOffsetRecorder, BranchRightOffsetRecorder class TestBranchAndJumpEvents(CheckEvents): maxDiff = None - @unittest.expectedFailure # TODO: RUSTPYTHON; - bytecode layout differs from CPython def test_loop(self): def func(): @@ -1710,7 +1710,6 @@ class TestBranchAndJumpEvents(CheckEvents): ('branch left', 'func', 12, 12)]) - @unittest.expectedFailure # TODO: RUSTPYTHON; - bytecode layout differs from CPython def test_match(self): def func(v=1): diff --git a/Lib/test/test_super.py b/Lib/test/test_super.py index cde2352e6..88e41780c 100644 --- a/Lib/test/test_super.py +++ b/Lib/test/test_super.py @@ -445,7 +445,6 @@ class TestSuper(unittest.TestCase): self.assertEqual(C().method(), super) - @unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: type 'super' is not an acceptable base type def test_super_subclass___class__(self): class mysuper(super): pass diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 6f06eb4ee..c2b6446ee 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -193,6 +193,8 @@ struct Compiler<'a> { /// 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 `. @@ -451,6 +453,96 @@ pub fn compile_top_with_syntax_warning_handler<'a>( } } +/// 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, +} + +/// compile.c _PyCompile_CodeGen +pub fn compile_codegen( + ast: ruff_python_ast::Mod, + source_file: SourceFile, + mode: Mode, + opts: CompileOpts, +) -> CompileResult { + 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 { + 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(), + "", + 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, @@ -1122,6 +1214,7 @@ impl<'warnings> Compiler<'warnings> { do_not_emit_bytecode: 0, disable_warning: 0, syntax_warning_handler, + save_nested_seqs: false, } } @@ -2153,8 +2246,14 @@ impl<'warnings> Compiler<'warnings> { let pop = self.code_stack.pop(); let stack_top = compiler_unwrap_option(self, pop); - // No parent scope stack to maintain - unwrap_internal(self, stack_top.finalize_code(&self.opts)) + 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) { @@ -2175,7 +2274,13 @@ impl<'warnings> Compiler<'warnings> { 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 } diff --git a/crates/codegen/src/error.rs b/crates/codegen/src/error.rs index 7092b6bb2..e6b57a2e4 100644 --- a/crates/codegen/src/error.rs +++ b/crates/codegen/src/error.rs @@ -29,6 +29,7 @@ pub enum InternalError { InvalidStackEffect, MalformedControlFlowGraph, MissingSymbol(String), + ConstIndexOutOfRange { index: usize, len: usize }, } impl Display for InternalError { @@ -42,6 +43,12 @@ impl Display for InternalError { f, "The symbol '{s}' must be present in the symbol table, even when it is undefined in python." ), + Self::ConstIndexOutOfRange { index, len } => { + write!( + f, + "LOAD_CONST index {index} is out of range for consts (len={len})" + ) + } } } } diff --git a/crates/codegen/src/ir.rs b/crates/codegen/src/ir.rs index bc25ca162..2c7aaedc0 100644 --- a/crates/codegen/src/ir.rs +++ b/crates/codegen/src/ir.rs @@ -204,6 +204,16 @@ impl ConstantPool { self.insert_full(constant).1 } + #[must_use] + pub fn from_ordered(constants: Vec) -> Self { + Self { constants } + } + + #[must_use] + pub fn into_vec(self) -> Vec { + self.constants + } + #[must_use] pub fn get_index(&self, idx: usize) -> Option<&ConstantData> { self.constants.get(idx) @@ -613,6 +623,7 @@ struct InstructionSequenceEntry { except_handler: InstructionSequenceExceptHandlerInfo, i_target: i32, i_offset: i32, + python_loc: Option<[i32; 4]>, } impl InstructionSequenceEntry { @@ -622,14 +633,15 @@ impl InstructionSequenceEntry { except_handler, i_target: 0, i_offset: 0, + python_loc: None, } } } const INSTRUCTION_SEQUENCE_UNSET_LABEL: i32 = -111; -#[derive(Clone)] -pub(crate) struct InstructionSequence { +#[derive(Clone, Default)] +pub struct InstructionSequence { /// CPython `instr_sequence.s_instrs`, including allocated slots beyond `s_used`. instrs: Vec, /// CPython `instr_sequence.s_allocated`, the allocated size of `s_instrs`. @@ -641,12 +653,116 @@ pub(crate) struct InstructionSequence { label_map: Option>, label_map_allocation: usize, annotations_code: Option>, + nested: Vec, +} + +/// One instruction as returned by InstructionSequence.get_instructions. +#[derive(Clone, Copy, Debug)] +pub struct PythonInstruction { + pub opcode: i32, + pub oparg: Option, + pub lineno: i32, + pub end_lineno: i32, + pub col_offset: i32, + pub end_col_offset: i32, } impl InstructionSequence { - pub(crate) fn new() -> Self { + #[must_use] + pub fn new() -> Self { instruction_sequence_new() } + + pub fn addop( + &mut self, + opcode: i32, + oparg: i32, + lineno: i32, + col_offset: i32, + end_lineno: i32, + end_col_offset: i32, + ) -> crate::InternalResult<()> { + let opcode = u16::try_from(opcode).map_err(|_| InternalError::MalformedControlFlowGraph)?; + if opcode > MAX_OPCODE { + return Err(InternalError::MalformedControlFlowGraph); + } + let opcode = + AnyOpcode::try_from(opcode).map_err(|_| InternalError::MalformedControlFlowGraph)?; + let instr: AnyInstruction = opcode.into(); + let oparg = u32::try_from(oparg).map_err(|_| InternalError::MalformedControlFlowGraph)?; + if oparg >= (1 << 30) || !(opcode.has_arg() || instr.has_target() || oparg == 0) { + return Err(InternalError::MalformedControlFlowGraph); + } + let loc = [lineno, col_offset, end_lineno, end_col_offset]; + let entry = + instruction_sequence_addop(self, instruction_info_from_python(instr, oparg, loc))?; + // addop arguments are (lineno, col_offset, end_lineno, end_col_offset); + // get_instructions reports (lineno, end_lineno, col_offset, end_col_offset) + // after storing those four values in struct order. + entry.python_loc = Some([lineno, col_offset, end_lineno, end_col_offset]); + Ok(()) + } + + pub fn new_label(&mut self) -> i32 { + instruction_sequence_new_label(self).0 + } + + pub fn use_label(&mut self, label: i32) -> crate::InternalResult<()> { + instruction_sequence_use_label(self, InstructionSequenceLabel(label)) + } + + pub fn apply_label_map(&mut self) { + instruction_sequence_apply_label_map(self); + } + + pub fn add_nested(&mut self, nested: Self) { + self.nested.push(nested); + } + + #[must_use] + pub fn nested(&self) -> &[Self] { + &self.nested + } + + pub fn nested_mut(&mut self) -> &mut Vec { + &mut self.nested + } + + #[must_use] + pub fn python_instructions(&self) -> Vec { + let mut out = Vec::with_capacity(self.instr_used); + for entry in &self.instrs[..self.instr_used] { + let opcode = any_opcode_as_i32(entry.info.instr.into()); + let has_arg = AnyOpcode::from(entry.info.instr).has_arg(); + let [lineno, end_lineno, col_offset, end_col_offset] = entry + .python_loc + .unwrap_or_else(|| python_location_of(&entry.info)); + out.push(PythonInstruction { + opcode, + oparg: has_arg.then_some(u32::from(entry.info.arg) as i32), + lineno, + end_lineno, + col_offset, + end_col_offset, + }); + } + out + } + + fn check_load_const_indices(&self, nconsts: usize) -> crate::InternalResult<()> { + for entry in &self.instrs[..self.instr_used] { + if matches!(entry.info.instr.real(), Some(Instruction::LoadConst { .. })) { + let index = u32::from(entry.info.arg) as usize; + if index >= nconsts { + return Err(InternalError::ConstIndexOutOfRange { + index, + len: nconsts, + }); + } + } + } + Ok(()) + } } /// instruction_sequence.c _PyInstructionSequence_New / inst_seq_create @@ -659,6 +775,67 @@ fn instruction_sequence_new() -> InstructionSequence { label_map: None, label_map_allocation: 0, annotations_code: None, + nested: Vec::new(), + } +} + +fn any_opcode_as_i32(opcode: AnyOpcode) -> i32 { + match opcode { + AnyOpcode::Real(op) => i32::from(u8::from(op)), + AnyOpcode::Pseudo(op) => i32::from(u16::from(op)), + } +} + +fn python_location_of(info: &InstructionInfo) -> [i32; 4] { + let loc = info.instruction_linetable_location(); + [loc.line, loc.end_line, loc.col, loc.end_col] +} + +fn instruction_info_from_python( + instr: AnyInstruction, + oparg: u32, + loc: [i32; 4], +) -> InstructionInfo { + let [lineno, col_offset, end_lineno, end_col_offset] = loc; + let lineno_override = match lineno.cmp(&0) { + core::cmp::Ordering::Less => Some(NO_LOCATION_OVERRIDE), + core::cmp::Ordering::Equal => Some(0), + core::cmp::Ordering::Greater => None, + }; + let line = if lineno > 0 { + OneIndexed::new(lineno as usize).unwrap_or(OneIndexed::MIN) + } else { + OneIndexed::MIN + }; + let end_line = if end_lineno > 0 { + OneIndexed::new(end_lineno as usize).unwrap_or(line) + } else { + line + }; + let col = if col_offset >= 0 { + OneIndexed::from_zero_indexed(col_offset as usize) + } else { + OneIndexed::MIN + }; + let end_col = if end_col_offset >= 0 { + OneIndexed::from_zero_indexed(end_col_offset as usize) + } else { + OneIndexed::MIN + }; + InstructionInfo { + instr, + arg: OpArg::new(oparg), + target: BlockIdx::NULL, + location: SourceLocation { + line, + character_offset: col, + }, + end_location: SourceLocation { + line: end_line, + character_offset: end_col, + }, + except_handler: None, + lineno_override, } } @@ -3954,6 +4131,105 @@ fn optimized_cfg_to_instruction_sequence( Ok((max_stackdepth, nlocalsplus, instr_sequence)) } +/// flowgraph.c _PyCompile_OptimizeCfg +pub fn optimize_cfg_for_tests( + seq: InstructionSequence, + consts: Vec, + nlocals: usize, +) -> crate::InternalResult<(InstructionSequence, Vec)> { + seq.check_load_const_indices(consts.len())?; + let mut metadata = CodeUnitMetadata { + name: String::new(), + qualname: None, + consts: ConstantPool::from_ordered(consts), + 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, + }; + let mut blocks = Blocks::from([Block::default()]); + optimize_code_unit(&mut metadata, &mut blocks, seq, nlocals, 0)?; + let _ = blocks.calculate_stackdepth()?; + blocks.optimize_load_fast()?; + let mut out = instruction_sequence_new(); + blocks.cfg_to_instruction_sequence(&mut out)?; + Ok((out, metadata.consts.into_vec())) +} + +/// compile.c _PyCompile_Assemble +pub fn assemble_for_tests( + filename: String, + seq: InstructionSequence, + metadata: CodeUnitMetadata, + debug_ranges: bool, +) -> crate::InternalResult { + let mut blocks = cfg_from_instruction_sequence(seq)?; + translate_jump_labels_to_targets(&mut blocks)?; + blocks.mark_except_handlers(); + label_exception_targets(&mut blocks)?; + let flags = CodeFlags::empty(); + let (max_stackdepth, nlocalsplus, mut instr_sequence) = + optimized_cfg_to_instruction_sequence(&metadata, flags, &mut blocks)?; + let localsplusinfo = compute_localsplus_info(&metadata, nlocalsplus, flags)?; + let CodeUnitMetadata { + name: obj_name, + qualname, + consts: constants, + names: name_cache, + varnames: varname_cache, + cellvars: _, + freevars: freevar_cache, + fast_hidden: _, + fast_hidden_final: _, + argcount: arg_count, + posonlyargcount: posonlyarg_count, + kwonlyargcount: kwonlyarg_count, + firstlineno: first_line_number, + } = metadata; + let code_arg_count = posonlyarg_count + .checked_add(arg_count) + .ok_or(InternalError::MalformedControlFlowGraph)?; + resolve_unconditional_jumps(&mut instr_sequence); + resolve_jump_offsets(&mut instr_sequence); + let assembled = assemble_emit( + &mut instr_sequence, + first_line_number.get() as i32, + debug_ranges, + )?; + let locations = rustpython_compiler_core::marshal::linetable_to_locations( + &assembled.linetable, + first_line_number.get() as i32, + assembled.instructions.len(), + ); + Ok(CodeObject { + flags, + posonlyarg_count, + arg_count: code_arg_count, + kwonlyarg_count, + source_path: filename, + first_line_number: Some(first_line_number), + obj_name: obj_name.clone(), + qualname: qualname.unwrap_or(obj_name), + max_stackdepth: max_stackdepth.max(1), + instructions: CodeUnits::from(assembled.instructions), + locations, + constants: constants.into_iter().collect(), + names: name_cache.into_iter().collect(), + varnames: varname_cache.into_iter().collect(), + cellvars: localsplusinfo.cellvars, + freevars: freevar_cache.into_iter().collect(), + localspluskinds: localsplusinfo.kinds, + linetable: assembled.linetable, + exceptiontable: assembled.exceptiontable, + }) +} + impl CodeInfo { pub fn finalize_code( mut self, diff --git a/crates/compiler-core/src/bytecode/instruction.rs b/crates/compiler-core/src/bytecode/instruction.rs index 63e292225..f403507f1 100644 --- a/crates/compiler-core/src/bytecode/instruction.rs +++ b/crates/compiler-core/src/bytecode/instruction.rs @@ -1416,6 +1416,9 @@ mod tests { assert!(PseudoOpcode::JumpIfTrue.has_arg()); assert!(PseudoOpcode::JumpNoInterrupt.has_arg()); assert!(PseudoOpcode::LoadClosure.has_arg()); + assert!(PseudoOpcode::SetupCleanup.has_arg()); + assert!(PseudoOpcode::SetupFinally.has_arg()); + assert!(PseudoOpcode::SetupWith.has_arg()); assert!(PseudoOpcode::StoreFastMaybeNull.has_arg()); assert!(!PseudoOpcode::AnnotationsPlaceholder.has_arg()); assert!(!PseudoOpcode::PopBlock.has_arg()); diff --git a/crates/compiler-core/src/bytecode/opcode_metadata.rs b/crates/compiler-core/src/bytecode/opcode_metadata.rs index 16db49bea..ca23e3c9e 100644 --- a/crates/compiler-core/src/bytecode/opcode_metadata.rs +++ b/crates/compiler-core/src/bytecode/opcode_metadata.rs @@ -929,6 +929,9 @@ impl super::PseudoOpcode { | Self::JumpIfTrue | Self::JumpNoInterrupt | Self::LoadClosure + | Self::SetupCleanup + | Self::SetupFinally + | Self::SetupWith | Self::StoreFastMaybeNull ) } diff --git a/crates/vm/src/builtins/code.rs b/crates/vm/src/builtins/code.rs index 9739cfe8e..2eb0e5a5f 100644 --- a/crates/vm/src/builtins/code.rs +++ b/crates/vm/src/builtins/code.rs @@ -1312,15 +1312,14 @@ impl PyCode { let (src, left, right) = match op { Instruction::ForIter { .. } => { // left = fall-through past CACHE entries (continue iteration) - // right = past END_FOR (iterator exhausted, skip cleanup) - // arg is relative forward from after instruction+caches + // right = next_offset + oparg + 2 (skip END_FOR and POP_ITER) let after_cache = i + 1 + caches; let target = after_cache + oparg as usize; let right = if matches!( instructions.get(target).map(|u| u.op), Some(Instruction::EndFor | Instruction::InstrumentedEndFor) ) { - (target + 1) * 2 + (target + 2) * 2 } else { target * 2 }; diff --git a/crates/vm/src/builtins/descriptor.rs b/crates/vm/src/builtins/descriptor.rs index 49fc4c865..e848353e8 100644 --- a/crates/vm/src/builtins/descriptor.rs +++ b/crates/vm/src/builtins/descriptor.rs @@ -349,7 +349,12 @@ fn get_slot_from_object( .get_slot(offset) .unwrap_or_else(|| vm.ctx.new_bool(false).into()), MemberKind::ObjectEx => obj.get_slot(offset).ok_or_else(|| { - vm.new_no_attribute_error(obj.clone(), vm.ctx.new_str(member.name.clone())) + // '%T' : module.qualname + vm.new_attribute_error(format!( + "'{}' object has no attribute '{}'", + obj.class().fully_qualified_name(vm), + member.name + )) })?, }; Ok(slot) @@ -654,11 +659,13 @@ impl SlotFunc { } Self::SetAttro(func) => { let (name, value): (PyRef, PyObjectRef) = args.bind(vm)?; + crate::types::hackcheck_setattro(&obj, *func, "__setattr__", vm)?; func(&obj, &name, PySetterValue::Assign(value), vm)?; Ok(vm.ctx.none()) } Self::DelAttro(func) => { let (name,): (PyRef,) = args.bind(vm)?; + crate::types::hackcheck_setattro(&obj, *func, "__delattr__", vm)?; func(&obj, &name, PySetterValue::Delete, vm)?; Ok(vm.ctx.none()) } diff --git a/crates/vm/src/builtins/dict.rs b/crates/vm/src/builtins/dict.rs index 3e8c1d4d5..0304d7cbe 100644 --- a/crates/vm/src/builtins/dict.rs +++ b/crates/vm/src/builtins/dict.rs @@ -649,7 +649,11 @@ impl AsMapping for PyDict { ass_subscript: atomic_func!(|mapping, needle, value, vm| { let zelf = PyDict::mapping_downcast(mapping); if let Some(value) = value { - zelf.inner_setitem(needle, value, vm) + zelf.inner_setitem(needle, value, vm)?; + if zelf.as_object().is(vm.builtins.dict().as_object()) { + crate::stdlib::_testinternalcapi::note_builtin_dict(); + } + Ok(()) } else { zelf.inner_delitem(needle, vm) } @@ -811,6 +815,15 @@ impl Py { Ok(attrs) } + pub(crate) fn get_item_known_hash( + &self, + key: &PyObject, + hash: crate::common::hash::PyHash, + vm: &VirtualMachine, + ) -> PyResult> { + self.entries.get_known_hash(vm, key, hash) + } + pub fn get_item_opt( &self, key: &K, @@ -927,7 +940,11 @@ impl Py { vm: &VirtualMachine, ) -> PyResult<()> { if self.exact_dict(vm) { - self.inner_setitem(key, value, vm) + self.inner_setitem(key, value, vm)?; + if self.as_object().is(vm.builtins.dict().as_object()) { + crate::stdlib::_testinternalcapi::note_builtin_dict(); + } + Ok(()) } else { self.as_object().set_item(key, value, vm) } diff --git a/crates/vm/src/builtins/function.rs b/crates/vm/src/builtins/function.rs index 5201fb7a2..a30ec5738 100644 --- a/crates/vm/src/builtins/function.rs +++ b/crates/vm/src/builtins/function.rs @@ -951,6 +951,7 @@ impl PyFunction { *jit_guard = None; } self.func_version.store(0, Relaxed); + crate::stdlib::_testinternalcapi::note_func_modification(); Ok(()) } @@ -965,6 +966,7 @@ impl PyFunction { PySetterValue::Delete => None, }; self.func_version.store(0, Relaxed); + crate::stdlib::_testinternalcapi::note_func_modification(); } #[pygetset] @@ -978,6 +980,7 @@ impl PyFunction { PySetterValue::Delete => None, }; self.func_version.store(0, Relaxed); + crate::stdlib::_testinternalcapi::note_func_modification(); } // {"__closure__", T_OBJECT, OFF(func_closure), READONLY}, diff --git a/crates/vm/src/builtins/object.rs b/crates/vm/src/builtins/object.rs index ca6201a8d..1e92fae76 100644 --- a/crates/vm/src/builtins/object.rs +++ b/crates/vm/src/builtins/object.rs @@ -441,6 +441,7 @@ impl PyBaseObject { if both_mutable || both_module { super::type_::compatible_for_assignment(current_cls, &cls, "__class__", vm)?; instance.set_class(cls, vm); + crate::stdlib::_testinternalcapi::note_set_class(); Ok(()) } else { Err(vm.new_type_error( @@ -517,6 +518,9 @@ pub(crate) fn object_set_dict( PySetterValue::Assign(value) => Some(downcast_dict(value, vm)?), PySetterValue::Delete => None, }; + if let Some(instance_dict) = obj.instance_dict() { + instance_dict.invalidate_inline_values(); + } obj.set_dict(dict) .map_err(|_| vm.new_attribute_error("This object has no __dict__")) } diff --git a/crates/vm/src/builtins/super.rs b/crates/vm/src/builtins/super.rs index f71bf51a6..07241e7b0 100644 --- a/crates/vm/src/builtins/super.rs +++ b/crates/vm/src/builtins/super.rs @@ -148,7 +148,10 @@ impl Initializer for PySuper { } } -#[pyclass(with(GetAttr, GetDescriptor, Constructor, Initializer, Representable))] +#[pyclass( + with(GetAttr, GetDescriptor, Constructor, Initializer, Representable), + flags(BASETYPE) +)] impl PySuper { #[pygetset] fn __thisclass__(&self) -> PyTypeRef { @@ -225,13 +228,7 @@ impl GetDescriptor for PySuper { .into_ref(&vm.ctx) .into()) } else { - let (obj, typ) = { - let lock = zelf.inner.read(); - let obj = lock.obj.as_ref().map(|(o, _)| o.to_owned()); - let typ = lock.typ.clone(); - (obj, typ) - }; - let obj = vm.unwrap_or_none(obj); + let typ = zelf.inner.read().typ.clone(); PyType::call(zelf.class(), (typ, obj).into_args(vm), vm) } } @@ -265,7 +262,7 @@ fn super_check(ty: PyTypeRef, obj: PyObjectRef, vm: &VirtualMachine) -> PyResult let class_attr = obj.get_attr("__class__", vm)?; if let Ok(cls) = class_attr.downcast::() - && !cls.is(&ty) + && !cls.is(obj.class()) && cls.fast_issubclass(&ty) { return Ok(cls); diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index fa0e70c8c..cf46d0730 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -31,6 +31,7 @@ use crate::{ use core::{ any::Any, borrow::Borrow, + cell::Cell, ops::Deref, pin::Pin, sync::atomic::{AtomicBool, AtomicPtr, AtomicU32, AtomicU64, Ordering}, @@ -616,15 +617,17 @@ fn downcast_qualname(value: PyObjectRef, vm: &VirtualMachine) -> PyResult, b: &Py) -> bool { - if a.is(b) { - return true; - } - for item in a_mro { - if item.is(b) { - return true; + if a_mro.is_empty() { + let mut current = Some(a); + while let Some(typ) = current { + if typ.is(b) { + return true; + } + current = typ.base.deref(); } + return false; } - false + a_mro.iter().any(|item| item.is(b)) } impl PyType { @@ -632,9 +635,24 @@ impl PyType { fn with_type_lock(vm: &VirtualMachine, f: impl FnOnce() -> R) -> R { // Drops deferred via try_defer_drop inside the critical section run // after the guard is released, outside the lock. + // + // The lock is reentrant on the same thread so a custom mro() can + // assign __bases__ (and re-enter here) without deadlocking. + thread_local! { + static HELD: Cell = const { Cell::new(false) }; + } rustpython_common::refcount::with_deferred_drops(|| { - let _guard = vm.state.type_mutex.lock(); - f() + HELD.with(|held| { + if held.get() { + f() + } else { + let _guard = vm.state.type_mutex.lock(); + held.set(true); + let result = f(); + held.set(false); + result + } + }) }) } @@ -704,6 +722,11 @@ impl PyType { } } + /// Store a specific `tp_version_tag` without going through assignment. + pub(crate) fn assign_specific_version(&self, version: u32) { + self.tp_version_tag.store(version, Ordering::Release); + } + pub fn modified(&self) { if self.tp_version_tag.load(Ordering::Acquire) == 0 { return; @@ -773,7 +796,16 @@ impl PyType { let bases = PyTuple::new_ref_typed(bases, ctx); let base = bases[0].clone(); - Self::new_heap_inner(base, bases, attrs, slots, heaptype_ext, metaclass, ctx) + Self::new_heap_inner( + base, + bases, + attrs, + slots, + heaptype_ext, + metaclass, + ctx, + false, + ) } /// Equivalent to CPython's PyType_Check macro @@ -945,24 +977,35 @@ impl PyType { heaptype_ext: HeapTypeExt, metaclass: PyRef, ctx: &Context, + defer_mro: bool, ) -> Result, String> { - let mro = Self::resolve_mro(&bases)?; + let mro = if defer_mro { + // Leave tp_mro unset so a custom metaclass mro() sees __mro__ is None. + Vec::new() + } else { + Self::resolve_mro(&bases)? + }; - // Inherit HAS_DICT from any base in MRO that has it - // (not just the first base, as any base with __dict__ means subclass needs it too) - if mro + // Layout flags come from each direct base's own flags. A custom + // mro() can omit a physical base from the stored MRO. + if bases .iter() .any(|b| b.slots.flags.has_feature(PyTypeFlags::HAS_DICT)) { - slots.flags |= PyTypeFlags::HAS_DICT + slots.flags |= PyTypeFlags::HAS_DICT; + } + if bases + .iter() + .any(|b| b.slots.flags.has_feature(PyTypeFlags::MANAGED_DICT)) + { + slots.flags |= PyTypeFlags::MANAGED_DICT; } - // Inherit HAS_WEAKREF/MANAGED_WEAKREF from any base in MRO that has it - if mro + if bases .iter() .any(|b| b.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF)) { - slots.flags |= PyTypeFlags::HAS_WEAKREF | PyTypeFlags::MANAGED_WEAKREF + slots.flags |= PyTypeFlags::HAS_WEAKREF | PyTypeFlags::MANAGED_WEAKREF; } // Inherit SEQUENCE and MAPPING flags from base classes @@ -1005,9 +1048,10 @@ impl PyType { metaclass, None, ); - new_type.mro.write().insert(0, new_type.clone()); - - new_type.init_slots(ctx); + if !defer_mro { + new_type.mro.write().insert(0, new_type.clone()); + new_type.init_slots(ctx); + } let weakref_type = super::PyWeak::static_type(); for base in new_type.bases.read().iter() { @@ -1100,7 +1144,12 @@ impl PyType { pub(crate) fn init_slots(&self, ctx: &Context) { // Inherit slots from MRO (mro[0] is self, so skip it) - let mro: Vec<_> = self.mro.read()[1..].to_vec(); + let mro_guard = self.mro.read(); + if mro_guard.is_empty() { + return; + } + let mro: Vec<_> = mro_guard[1..].to_vec(); + drop(mro_guard); for base in &mro { self.inherit_slots(base); } @@ -1425,7 +1474,8 @@ impl PyType { } pub fn get_super_attr(&self, attr_name: &'static PyStrInterned) -> Option { - self.mro.read()[1..] + let mro = self.mro.read(); + mro.get(1..)? .iter() .find_map(|class| class.attributes.get(attr_name)) } @@ -1606,7 +1656,11 @@ impl Py { /// so only use this if `cls` is known to have not overridden the base __subclasscheck__ magic /// method. pub fn fast_issubclass(&self, cls: &impl Borrow) -> bool { - self.as_object().is(cls.borrow()) || self.mro.read()[1..].iter().any(|c| c.is(cls.borrow())) + let mro = self.mro.read(); + if mro.is_empty() { + return self.as_object().is(cls.borrow()); + } + mro.iter().any(|c| c.is(cls.borrow())) } pub fn mro_map_collect(&self, f: F) -> Vec @@ -1682,8 +1736,6 @@ impl PyType { } let bases = bases_tuple.try_into_typed::(vm)?; - // TODO: check for mro cycles - // Compute the new solid base before committing anything. This also // validates the new bases (BASETYPE flag, no instance layout // conflict), the same checks type creation performs. @@ -1728,6 +1780,14 @@ impl PyType { }; let result = Self::with_type_lock(vm, || { + for base in bases.iter() { + if is_subtype_with_mro(&base.mro.read(), base, zelf) + || (!base.mro.read().is_empty() && type_is_subtype_base_chain(base, zelf, vm)) + { + return Err(vm.new_type_error("a __bases__ item causes an inheritance cycle")); + } + } + // Remove this class from the old bases' subclass lists, pruning // dead entries along the way. Upgraded refs are retired so the // last strong reference is never dropped under the lock. @@ -1759,22 +1819,27 @@ impl PyType { cls: &Py, undo: &mut Vec<(PyTypeRef, Vec)>, vm: &VirtualMachine, - ) -> PyResult<()> { - let mut mro = - PyType::resolve_mro(&cls.bases.read()).map_err(|msg| vm.new_type_error(msg))?; - // Preserve self (mro[0]) when updating MRO - mro.insert(0, cls.mro.read()[0].to_owned()); - let old_mro = core::mem::replace(&mut *cls.mro.write(), mro); - undo.push((cls.to_owned(), old_mro)); - for subclass in cls.subclasses.read().iter() { - // Dead entries are pruned elsewhere; skip them here. - let Some(subclass) = subclass.upgrade() else { - continue; - }; - let subclass: &Py = subclass.downcast_ref().unwrap(); - update_mro_recursively(subclass, undo, vm)?; + ) -> PyResult { + let old_ptr = cls.mro.read().as_ptr(); + let new_mro = mro_invoke(cls, vm)?; + if cls.mro.read().as_ptr() != old_ptr { + // A nested custom mro() already replaced tp_mro. + return Ok(0); } - Ok(()) + let old_mro = core::mem::replace(&mut *cls.mro.write(), new_mro); + undo.push((cls.to_owned(), old_mro)); + cls.modified_inner(); + let subclasses: Vec = cls + .subclasses + .read() + .iter() + .filter_map(|subclass| subclass.upgrade()) + .filter_map(|subclass| subclass.downcast::().ok()) + .collect(); + for subclass in subclasses { + update_mro_recursively(&subclass, undo, vm)?; + } + Ok(1) } let mut undo = Vec::new(); if let Err(err) = update_mro_recursively(zelf, &mut undo, vm) { @@ -1811,6 +1876,7 @@ impl PyType { zelf.update_all_slots(&vm.ctx); register_subclasses(&zelf.bases.read()); + crate::stdlib::_testinternalcapi::note_set_bases(); Ok(()) }); drop(retired); @@ -2171,9 +2237,10 @@ impl PyType { #[pygetset(setter)] fn set___dict__(&self, _value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { - Err(vm.new_not_implemented_error( - "Setting __dict__ attribute on a type isn't yet implemented", - )) + Err(vm.new_attribute_error(format!( + "attribute '__dict__' of '{}' objects is not writable", + self.name() + ))) } fn check_set_special_type_attr( @@ -2482,8 +2549,13 @@ impl Constructor for PyType { seen_weakref = true; } - // Check if slot name conflicts with class attributes - if attributes.contains_key(vm.ctx.intern_str(slot.as_wtf8())) { + // __qualname__ and __classcell__ are written by the compiler + // and may share a name with a slot. + if slot_name != b"__qualname__" + && slot_name != b"__classcell__" + && slot_name != b"__classdictcell__" + && attributes.contains_key(vm.ctx.intern_str(slot.as_wtf8())) + { return Err(vm.new_value_error(format!( "'{}' in __slots__ conflicts with class variable", slot.as_wtf8() @@ -2508,17 +2580,15 @@ impl Constructor for PyType { let add_weakref = seen_weakref; // Filter out __dict__ and __weakref__ from slots - // (they become descriptors, not member slots) - let filtered_slots = if has_dict || add_weakref { - let filtered: Vec = slots - .iter() - .filter(|s| s.as_wtf8() != dict_name && s.as_wtf8() != weakref_name) - .cloned() - .collect(); - PyTuple::new_ref_typed(filtered, &vm.ctx) - } else { - slots - }; + // (they become descriptors, not member slots), then sort so + // __class__ assignment can compare layouts by slot name. + let mut filtered: Vec = slots + .iter() + .filter(|s| s.as_wtf8() != dict_name && s.as_wtf8() != weakref_name) + .cloned() + .collect(); + filtered.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes())); + let filtered_slots = PyTuple::new_ref_typed(filtered, &vm.ctx); (Some(filtered_slots), has_dict, add_weakref) } else { @@ -2546,6 +2616,11 @@ impl Constructor for PyType { // 2. __dict__ is in __slots__ if (heaptype_slots.is_none() && may_add_dict) || add_dict { flags |= PyTypeFlags::HAS_DICT | PyTypeFlags::MANAGED_DICT; + // type_ready_managed_dict: fixed-size managed-dict types + // get an inline values array after the object. + if base.slots.itemsize == 0 { + flags |= PyTypeFlags::INLINE_VALUES; + } } // Add HAS_WEAKREF if: @@ -2574,6 +2649,7 @@ impl Constructor for PyType { (slots, heaptype_ext) }; + let custom_mro = !metatype.is(vm.ctx.types.type_type); let typ = Self::new_heap_inner( base, bases, @@ -2582,9 +2658,42 @@ impl Constructor for PyType { heaptype_ext, metatype, &vm.ctx, + custom_mro, ) .map_err(|e| vm.new_type_error(e))?; + if custom_mro { + mro_internal(&typ, vm)?; + typ.init_slots(&vm.ctx); + } + + // Consume __classcell__ before slot members of the same name + // overwrite the compiler-provided cell. + if let Some(cell) = typ.attributes.get(identifier!(vm, __classcell__)) { + let cell = PyCellRef::try_from_object(vm, cell.clone()).map_err(|_| { + vm.new_type_error(format!( + "__classcell__ must be a nonlocal cell, not {}", + cell.class().name() + )) + })?; + cell.set(Some(typ.clone().into())); + typ.attributes.remove(identifier!(vm, __classcell__)); + } + if let Some(cell) = typ.attributes.get(identifier!(vm, __classdictcell__)) { + let cell = PyCellRef::try_from_object(vm, cell.clone()).map_err(|_| { + vm.new_type_error(format!( + "__classdictcell__ must be a nonlocal cell, not {}", + cell.class().name() + )) + })?; + let namespace = typ + .attributes + .as_dict() + .expect("a type built by type.__new__ has a dict namespace"); + cell.set(Some(namespace.clone().into())); + typ.attributes.remove(identifier!(vm, __classdictcell__)); + } + if let Some(ref slots) = heaptype_slots { let class_name = typ.name().to_string(); for (offset, member) in (base_member_count..).zip(slots.as_slice().iter()) { @@ -2616,34 +2725,6 @@ impl Constructor for PyType { } } - if let Some(cell) = typ.attributes.get(identifier!(vm, __classcell__)) { - let cell = PyCellRef::try_from_object(vm, cell.clone()).map_err(|_| { - vm.new_type_error(format!( - "__classcell__ must be a nonlocal cell, not {}", - cell.class().name() - )) - })?; - cell.set(Some(typ.clone().into())); - typ.attributes.remove(identifier!(vm, __classcell__)); - } - if let Some(cell) = typ.attributes.get(identifier!(vm, __classdictcell__)) { - let cell = PyCellRef::try_from_object(vm, cell.clone()).map_err(|_| { - vm.new_type_error(format!( - "__classdictcell__ must be a nonlocal cell, not {}", - cell.class().name() - )) - })?; - // Annotation scopes look names up here long after the class body ran, - // so the cell gets the type's own namespace rather than the dict passed - // to __new__: later attribute changes have to be visible through it. - let namespace = typ - .attributes - .as_dict() - .expect("a type built by type.__new__ has a dict namespace"); - cell.set(Some(namespace.clone().into())); - typ.attributes.remove(identifier!(vm, __classdictcell__)); - } - // All *classes* should have a dict. Exceptions are *instances* of // classes that define __slots__ and instances of built-in classes // (with exceptions, e.g function) @@ -2727,12 +2808,16 @@ impl Constructor for PyType { })?; } - if let Some(init_subclass) = typ.get_super_attr(identifier!(vm, __init_subclass__)) { - let init_subclass = vm - .call_get_descriptor_specific(&init_subclass, None, Some(typ.clone().into())) - .unwrap_or(Ok(init_subclass))?; - init_subclass.call(kwargs, vm)?; - }; + // type_new_init_subclass: super(type, type).__init_subclass__(**kwds) + let super_obj = vm + .ctx + .types + .super_type + .as_object() + .call((typ.clone(), typ.clone()), vm)?; + super_obj + .get_attr(identifier!(vm, __init_subclass__), vm)? + .call(kwargs, vm)?; Ok(typ.into()) } @@ -2855,9 +2940,14 @@ impl GetAttr for PyType { #[pyclass] impl Py { #[pygetset] - fn __mro__(&self) -> PyTuple { - let elements: Vec = self.mro_map_collect(|x| x.as_object().to_owned()); - PyTuple::new_unchecked(elements.into_boxed_slice()) + fn __mro__(&self, vm: &VirtualMachine) -> PyObjectRef { + let mro = self.mro.read(); + if mro.is_empty() { + return vm.ctx.none(); + } + let elements: Vec = mro.iter().map(|x| x.as_object().to_owned()).collect(); + drop(mro); + vm.ctx.new_tuple(elements).into() } #[pygetset] @@ -2937,8 +3027,11 @@ impl Py { } #[pymethod] - fn mro(&self) -> Vec { - self.mro_map_collect(|cls| cls.to_owned().into()) + fn mro(&self, vm: &VirtualMachine) -> PyResult> { + Ok(mro_implementation(self, vm)? + .into_iter() + .map(Into::into) + .collect()) } } @@ -3392,6 +3485,86 @@ fn calculate_meta_class( Ok(winner) } +fn type_is_subtype_base_chain(a: &Py, b: &Py, vm: &VirtualMachine) -> bool { + let mut current = Some(a); + while let Some(typ) = current { + if typ.is(b) { + return true; + } + current = typ.base.deref(); + } + b.is(vm.ctx.types.object_type) +} + +fn mro_implementation(typ: &Py, vm: &VirtualMachine) -> PyResult> { + for base in typ.bases.read().iter() { + if base.mro.read().is_empty() { + return Err(vm.new_type_error(format!( + "Cannot extend an incomplete type '{}'", + base.name() + ))); + } + } + let mut mro = PyType::resolve_mro(&typ.bases.read()).map_err(|msg| vm.new_type_error(msg))?; + mro.insert(0, typ.to_owned()); + Ok(mro) +} + +fn mro_check(typ: &Py, mro: &[PyObjectRef], vm: &VirtualMachine) -> PyResult<()> { + let solid = solid_base(typ, vm).to_owned(); + for obj in mro { + let Some(base) = obj.downcast_ref::() else { + return Err(vm.new_type_error(format!( + "mro() returned a non-class ('{}')", + obj.class().name() + ))); + }; + if !is_subtype_with_mro(&solid.mro.read(), &solid, solid_base(base, vm)) { + return Err(vm.new_type_error(format!( + "mro() returned base with unsuitable layout ('{}')", + base.slot_name() + ))); + } + } + Ok(()) +} + +fn mro_invoke(typ: &Py, vm: &VirtualMachine) -> PyResult> { + let custom = !typ.class().is(vm.ctx.types.type_type); + if !custom { + return mro_implementation(typ, vm); + } + + let mro_result = vm.call_special_method(typ.as_object(), identifier!(vm, mro), ())?; + let items: Vec = mro_result.try_to_value(vm)?; + if items.is_empty() { + return Err(vm.new_type_error("type MRO must not be empty")); + } + mro_check(typ, &items, vm)?; + items + .into_iter() + .map(|obj| { + obj.downcast::().map_err(|obj| { + vm.new_type_error(format!( + "mro() returned a non-class ('{}')", + obj.class().name() + )) + }) + }) + .collect() +} + +fn mro_internal(typ: &Py, vm: &VirtualMachine) -> PyResult { + let old_ptr = typ.mro.read().as_ptr(); + let new_mro = mro_invoke(typ, vm)?; + if typ.mro.read().as_ptr() != old_ptr { + return Ok(0); + } + *typ.mro.write() = new_mro; + typ.modified(); + Ok(1) +} + /// Returns true if the two types have different instance layouts. fn shape_differs(t1: &Py, t2: &Py) -> bool { t1.__basicsize__() != t2.__basicsize__() || t1.slots.itemsize != t2.slots.itemsize @@ -3492,8 +3665,7 @@ fn same_slots_added(a: &Py, b: &Py) -> bool { .zip(y.iter()) .all(|(p, q)| p.as_wtf8() == q.as_wtf8()) } - (None, None) => true, - _ => false, + _ => true, } } diff --git a/crates/vm/src/dict_inner.rs b/crates/vm/src/dict_inner.rs index 76d2c50f0..7c271bd7d 100644 --- a/crates/vm/src/dict_inner.rs +++ b/crates/vm/src/dict_inner.rs @@ -58,6 +58,11 @@ static KEYS_VERSION: AtomicU32 = AtomicU32::new(0); /// Allocate a new keys-version stamp. Returns 0 once the stamp space is /// exhausted; stamps are only allocated on specialization, so exhaustion is /// unrealistic in practice. +/// Next keys-version stamp that [`next_keys_version`] would assign. +pub(crate) fn peek_next_keys_version() -> u32 { + KEYS_VERSION.load(Relaxed).saturating_add(1) +} + fn next_keys_version() -> u32 { KEYS_VERSION .try_update(Relaxed, Relaxed, |v| v.checked_add(1)) @@ -573,6 +578,16 @@ impl Dict { self._get_inner(vm, key, hash) } + /// `_PyDict_GetItem_KnownHash`: lookup using a caller-supplied hash. + pub(crate) fn get_known_hash( + &self, + vm: &VirtualMachine, + key: &K, + hash: HashValue, + ) -> PyResult> { + self._get_inner(vm, key, hash) + } + /// Return a stable entry hint for `key` if present. /// /// The hint is the internal entry index and can be used with diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 34d026d01..949bbf5e9 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -7206,6 +7206,13 @@ impl ExecutingFrame<'_> { self.push_value(value); } Ok(PyIterReturn::StopIteration(value)) => { + // FOR_ITER_GEN returns through END_FOR, which + // fires STOP_ITERATION rather than RAISE. + if self.monitoring_mask & monitoring::EVENT_STOP_ITERATION != 0 { + let offset = (self.lasti() - 1) * 2; + let val = vm.unwrap_or_none(value.clone()); + monitoring::fire_stop_iteration(vm, self.code, offset, &val)?; + } if vm.use_tracing.get() && self.trace_is_set(vm) { let stop_exc = vm.new_stop_iteration(value); self.fire_exception_trace(&stop_exc, vm)?; @@ -7453,7 +7460,9 @@ impl ExecutingFrame<'_> { monitoring::fire_branch_left(vm, self.code, src_offset, dest_offset)?; } } else if self.monitoring_mask & monitoring::EVENT_BRANCH_RIGHT != 0 { - let dest_offset = self.lasti() * 2; + // INSTRUMENTED_POP_ITER: dest is the instruction after + // POP_ITER (FOR_ITER jumps over END_FOR onto POP_ITER). + let dest_offset = (self.lasti() + 1) * 2; monitoring::fire_branch_right(vm, self.code, src_offset, dest_offset)?; } Ok(None) @@ -8497,6 +8506,39 @@ impl ExecutingFrame<'_> { Ok(None) } + /// `_PyEval_MonitorRaise` for a StopIteration produced by FOR_ITER. + /// Sequence iterators match FOR_ITER_LIST/RANGE/TUPLE and do not raise. + /// Generators still report RAISE and the settrace exception event + /// (Internal StopIteration). + fn monitor_for_iter_stop( + &self, + value: Option, + vm: &VirtualMachine, + ) -> PyResult<()> { + let iter = self.top_value(); + if iter.downcast_ref_if_exact::(vm).is_some() + || iter.downcast_ref_if_exact::(vm).is_some() + || iter.downcast_ref_if_exact::(vm).is_some() + { + return Ok(()); + } + let need_raise = self.monitoring_mask & monitoring::EVENT_RAISE != 0; + let need_trace = vm.use_tracing.get() && self.trace_is_set(vm); + if !need_raise && !need_trace { + return Ok(()); + } + let stop_exc = vm.new_stop_iteration(value); + if need_raise { + let offset = (self.lasti() - 1) * 2; + let exc_obj: PyObjectRef = stop_exc.clone().into(); + monitoring::fire_raise(vm, self.code, offset, &exc_obj)?; + } + if need_trace { + self.fire_exception_trace(&stop_exc, vm)?; + } + Ok(()) + } + /// Advance the iterator on top of stack. /// Returns `true` if iteration continued (item pushed), `false` if exhausted (jumped). fn execute_for_iter( @@ -8529,12 +8571,9 @@ impl ExecutingFrame<'_> { Ok(true) } Ok(PyIterReturn::StopIteration(value)) => { - // Fire 'exception' trace event for StopIteration, matching - // FOR_ITER's inline call to _PyEval_MonitorRaise. - if vm.use_tracing.get() && self.trace_is_set(vm) { - let stop_exc = vm.new_stop_iteration(value); - self.fire_exception_trace(&stop_exc, vm)?; - } + // _FOR_ITER / INSTRUMENTED_FOR_ITER: _PyEval_MonitorRaise + // then clear the StopIteration and jump over END_FOR. + self.monitor_for_iter_stop(value, vm)?; self.jump(self.for_iter_jump_target(target)); Ok(false) } diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index 84671c074..1ec5777e0 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -372,10 +372,15 @@ pub(super) struct ObjExt { } impl ObjExt { - fn new(dict: Option, member_count: usize, has_dict: bool) -> Self { + fn new( + dict: Option, + member_count: usize, + has_dict: bool, + inline_values: bool, + ) -> Self { Self { dict: if has_dict { - Some(InstanceDict::from_opt(dict)) + Some(InstanceDict::from_opt(dict, inline_values)) } else { None }, @@ -1079,9 +1084,14 @@ impl Py { } } +/// SHARED_KEYS_MAX_SIZE: beyond this many keys, inline values are +/// converted to a regular heap dict. +pub(crate) const SHARED_KEYS_MAX_SIZE: usize = 30; + #[derive(Debug)] pub(crate) struct InstanceDict { pub(crate) d: PyRwLock>, + inline_values_valid: PyAtomic, } impl From for InstanceDict { @@ -1093,16 +1103,35 @@ impl From for InstanceDict { impl InstanceDict { #[inline] - pub(crate) const fn new(d: PyDictRef) -> Self { + pub(crate) fn new(d: PyDictRef) -> Self { + Self::from_opt(Some(d), false) + } + + #[inline] + pub(crate) fn from_opt(d: Option, inline_values: bool) -> Self { Self { - d: PyRwLock::new(Some(d)), + d: PyRwLock::new(d), + inline_values_valid: Radium::new(inline_values), } } #[inline] - pub(crate) const fn from_opt(d: Option) -> Self { - Self { - d: PyRwLock::new(d), + pub(crate) fn inline_values_valid(&self) -> bool { + self.inline_values_valid.load(Ordering::Relaxed) + } + + #[inline] + pub(crate) fn invalidate_inline_values(&self) { + self.inline_values_valid.store(false, Ordering::Relaxed); + } + + pub(crate) fn maybe_materialize_inline_values(&self) { + if !self.inline_values_valid() { + return; + } + let overflow = self.with(|d| d.is_some_and(|d| d.__len__() > SHARED_KEYS_MAX_SIZE)); + if overflow { + self.invalidate_inline_values(); } } @@ -1283,11 +1312,10 @@ impl PyInner { unsafe { if let Some(offset) = ext_start { let ext_ptr = alloc_ptr.add(offset) as *mut ObjExt; - let has_dict = typ - .slots - .flags - .has_feature(crate::types::PyTypeFlags::HAS_DICT); - ext_ptr.write(ObjExt::new(dict, member_count, has_dict)); + let flags = typ.slots.flags; + let has_dict = flags.has_feature(crate::types::PyTypeFlags::HAS_DICT); + let inline_values = flags.has_feature(crate::types::PyTypeFlags::INLINE_VALUES); + ext_ptr.write(ObjExt::new(dict, member_count, has_dict, inline_values)); } if let Some(offset) = weakref_start { @@ -1700,6 +1728,18 @@ impl PyObject { self.0.ext_ref().and_then(|ext| ext.dict.as_ref()) } + /// `_PyObject_InlineValues(obj)->valid` when the type has INLINE_VALUES. + #[inline] + pub fn has_inline_values(&self) -> bool { + self.class() + .slots + .flags + .has_feature(crate::types::PyTypeFlags::INLINE_VALUES) + && self + .instance_dict() + .is_some_and(InstanceDict::inline_values_valid) + } + #[inline(always)] pub fn dict(&self) -> Option { self.instance_dict().and_then(|d| d.get()) @@ -2789,7 +2829,7 @@ pub(crate) fn init_type_hierarchy() -> BootstrapTypeHierarchy { alloc_ptr.expose_provenance(); unsafe { - (alloc_ptr as *mut ObjExt).write(ObjExt::new(None, 0, true)); + (alloc_ptr as *mut ObjExt).write(ObjExt::new(None, 0, true, false)); (alloc_ptr.add(weakref_offset) as *mut WeakRefList).write(WeakRefList::new()); alloc_ptr.add(inner_offset).cast() } diff --git a/crates/vm/src/protocol/object.rs b/crates/vm/src/protocol/object.rs index bcb00920c..fc97f4462 100644 --- a/crates/vm/src/protocol/object.rs +++ b/crates/vm/src/protocol/object.rs @@ -227,6 +227,7 @@ impl PyObject { instance_dict .get_or_insert(vm) .set_item(attr_name, value, vm)?; + instance_dict.maybe_materialize_inline_values(); } else if let Some(dict) = instance_dict.get() { dict.del_item(attr_name, vm).map_err(|e| { if e.fast_isinstance(vm.ctx.exceptions.key_error) { @@ -603,8 +604,8 @@ impl PyObject { return Ok(false); } - // Check for __subclasscheck__ method using lookup_special (matches CPython) - if let Some(checker) = cls.lookup_special(identifier!(vm, __subclasscheck__), vm) { + // Check for __subclasscheck__ method using lookup_special + if let Some(checker) = cls.lookup_special(identifier!(vm, __subclasscheck__), vm)? { let res = vm.with_recursion("in __subclasscheck__", || { checker.call((derived.to_owned(),), vm) })?; @@ -695,8 +696,8 @@ impl PyObject { return Ok(false); } - // Check for __instancecheck__ method using lookup_special (matches CPython) - if let Some(checker) = cls.lookup_special(identifier!(vm, __instancecheck__), vm) { + // Check for __instancecheck__ method using lookup_special + if let Some(checker) = cls.lookup_special(identifier!(vm, __instancecheck__), vm)? { let res = vm.with_recursion("in __instancecheck__", || { checker.call((self.to_owned(),), vm) })?; @@ -833,23 +834,26 @@ impl PyObject { Err(vm.new_type_error(msg)) } - /// Equivalent to CPython's _PyObject_LookupSpecial - /// Looks up a special method in the type's MRO without checking instance dict. - /// Returns None if not found (masking AttributeError like CPython). - pub fn lookup_special(&self, attr: &Py, vm: &VirtualMachine) -> Option { + /// _PyObject_LookupSpecial: look up a special method in the type MRO + /// without checking the instance dict. A miss is silent; a descr_get + /// error is propagated. + pub fn lookup_special( + &self, + attr: &Py, + vm: &VirtualMachine, + ) -> PyResult> { let obj_cls = self.class(); - // Use PyType::lookup_ref (equivalent to CPython's _PyType_LookupRef) - let res = obj_cls.lookup_ref(attr, vm)?; + let Some(res) = obj_cls.lookup_ref(attr, vm) else { + return Ok(None); + }; - // If it's a descriptor, call its __get__ method let descr_get = res.class().slots.descr_get.load(); if let Some(descr_get) = descr_get { let obj_cls = obj_cls.to_owned().into(); - // CPython ignores exceptions in _PyObject_LookupSpecial and returns NULL - descr_get(res, Some(self.to_owned()), Some(obj_cls), vm).ok() + descr_get(res, Some(self.to_owned()), Some(obj_cls), vm).map(Some) } else { - Some(res) + Ok(Some(res)) } } } diff --git a/crates/vm/src/stdlib/_ast.rs b/crates/vm/src/stdlib/_ast.rs index 567cf8b73..f44281ade 100644 --- a/crates/vm/src/stdlib/_ast.rs +++ b/crates/vm/src/stdlib/_ast.rs @@ -2439,6 +2439,45 @@ pub(crate) fn preprocess_ast_object( Ok(result) } +#[cfg(feature = "codegen")] +pub(crate) struct RustModFromObject { + pub ast: ast::Mod, + pub source_file: SourceFile, +} + +#[cfg(feature = "codegen")] +pub(crate) fn rust_mod_from_object( + vm: &VirtualMachine, + object: PyObjectRef, + filename: &str, +) -> PyResult { + let is_mod = object + .class() + .fast_issubclass(pyast::NodeMod::static_type()) + || object.is_instance(pyast::NodeMod::static_type().as_object(), vm)?; + if !is_mod && !is_ast_instance(vm, &object)? { + return Err(vm.new_type_error("expected an AST")); + } + let text = synthetic_source_from_ast_object(vm, &object)?; + let source_file = SourceFileBuilder::new(filename.to_owned(), text).finish(); + let ast = Node::ast_from_object(vm, &source_file, object)?; + validate::validate_mod(vm, &ast)?; + let ast = match ast { + Mod::Module(m) => ast::Mod::Module(m.module), + Mod::Interactive(ModInteractive { range, body, .. }) => ast::Mod::Module(ast::ModModule { + node_index: Default::default(), + range, + body, + runtime_body: None, + }), + Mod::Expression(e) => ast::Mod::Expression(e), + Mod::FunctionType(_) => { + return Err(vm.new_runtime_error("this compiler does not handle FunctionTypes")); + } + }; + Ok(RustModFromObject { ast, source_file }) +} + #[cfg(feature = "codegen")] pub(crate) fn compile( vm: &VirtualMachine, diff --git a/crates/vm/src/stdlib/_interpreters.rs b/crates/vm/src/stdlib/_interpreters.rs index 0df148364..02276c363 100644 --- a/crates/vm/src/stdlib/_interpreters.rs +++ b/crates/vm/src/stdlib/_interpreters.rs @@ -2,7 +2,7 @@ //! //! Mirrors CPython `Modules/_interpretersmodule.c`. -pub(crate) use _interpreters::{init_xi_types, module_def}; +pub(crate) use _interpreters::{config_from_pyobject, init_xi_types, module_def}; #[cfg_attr(not(feature = "threading"), allow(unused_imports))] pub(crate) use _interpreters::{ interpreter_error, interpreter_not_found, not_shareable_error, xibufferview_from_buffer, @@ -342,6 +342,14 @@ pub(crate) mod _interpreters { .ok_or_else(|| vm.new_value_error(format!("unsupported config name '{name}'"))) } + /// `config_from_object`. + pub(crate) fn config_from_pyobject( + obj: &PyObject, + vm: &VirtualMachine, + ) -> PyResult { + parse_config(Some(obj), vm) + } + /// `config_from_object`. fn parse_config(obj: Option<&PyObject>, vm: &VirtualMachine) -> PyResult { let Some(obj) = obj.filter(|o| !vm.is_none(o)) else { diff --git a/crates/vm/src/stdlib/_testinternalcapi.rs b/crates/vm/src/stdlib/_testinternalcapi.rs new file mode 100644 index 000000000..31b65521c --- /dev/null +++ b/crates/vm/src/stdlib/_testinternalcapi.rs @@ -0,0 +1,1915 @@ +pub(crate) use _testinternalcapi::module_def; + +use core::sync::atomic::{AtomicUsize, Ordering}; + +static RARE_SET_CLASS: AtomicUsize = AtomicUsize::new(0); +static RARE_SET_BASES: AtomicUsize = AtomicUsize::new(0); +static RARE_SET_EVAL_FRAME_FUNC: AtomicUsize = AtomicUsize::new(0); +static RARE_BUILTIN_DICT: AtomicUsize = AtomicUsize::new(0); +static RARE_FUNC_MODIFICATION: AtomicUsize = AtomicUsize::new(0); + +pub(crate) fn note_set_class() { + RARE_SET_CLASS.fetch_add(1, Ordering::Relaxed); +} + +pub(crate) fn note_set_bases() { + RARE_SET_BASES.fetch_add(1, Ordering::Relaxed); +} + +pub(crate) fn note_builtin_dict() { + RARE_BUILTIN_DICT.fetch_add(1, Ordering::Relaxed); +} + +pub(crate) fn note_func_modification() { + RARE_FUNC_MODIFICATION.fetch_add(1, Ordering::Relaxed); +} + +#[pymodule] +mod _testinternalcapi { + use super::*; + use crate::{ + AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + atomic_func, + builtins::{ + PyBytesRef, PyCode, PyDict, PyDictRef, PyStrRef, PyType, PyTypeRef, + descriptor::PyWrapper, + }, + common::{hash::PyHash, lock::LazyLock}, + dict_inner, + frame::FrameObject, + function::{OptionalArg, PyComparisonValue}, + object::{Traverse, TraverseFn}, + protocol::{PyIterReturn, PyMappingMethods, PySequenceMethods}, + types::{ + AsMapping, AsSequence, Comparable, IterNext, Iterable, PyComparisonOp, PyTypeFlags, + SelfIter, + }, + }; + use std::collections::HashMap; + + // ADAPTIVE_WARMUP_VALUE + 1 + #[pyattr] + const SPECIALIZATION_THRESHOLD: usize = 2; + + // ADAPTIVE_COOLDOWN_VALUE + 1 + #[pyattr] + const SPECIALIZATION_COOLDOWN: usize = 53; + + #[pyattr] + const SHARED_KEYS_MAX_SIZE: usize = crate::object::SHARED_KEYS_MAX_SIZE; + + // Free-threaded builds do not prefix objects with PyGC_Head. + #[pyattr] + const SIZEOF_PYGC_HEAD: usize = 0; + + #[pyattr] + const SIZEOF_MANAGED_PRE_HEADER: usize = 2 * core::mem::size_of::<*const ()>(); + + #[pyattr] + const SIZEOF_PYOBJECT: usize = core::mem::size_of::(); + + #[pyattr] + const SIZEOF_TIME_T: usize = 8; + + // JUMP_BACKWARD_INITIAL_VALUE + 1 + #[pyattr] + const TIER2_THRESHOLD: usize = 4096; + + #[pyfunction] + fn has_inline_values(obj: PyObjectRef, _vm: &VirtualMachine) -> bool { + obj.has_inline_values() + } + + #[pyfunction] + fn get_recursion_depth(vm: &VirtualMachine) -> usize { + vm.current_recursion_depth() + } + + #[pyfunction] + fn reset_rare_event_counters() { + RARE_SET_CLASS.store(0, Ordering::Relaxed); + RARE_SET_BASES.store(0, Ordering::Relaxed); + RARE_SET_EVAL_FRAME_FUNC.store(0, Ordering::Relaxed); + RARE_BUILTIN_DICT.store(0, Ordering::Relaxed); + RARE_FUNC_MODIFICATION.store(0, Ordering::Relaxed); + } + + #[pyfunction] + fn get_rare_event_counters(vm: &VirtualMachine) -> PyResult { + let dict = vm.ctx.new_dict(); + dict.set_item( + "set_class", + vm.ctx + .new_int(RARE_SET_CLASS.load(Ordering::Relaxed)) + .into(), + vm, + )?; + dict.set_item( + "set_bases", + vm.ctx + .new_int(RARE_SET_BASES.load(Ordering::Relaxed)) + .into(), + vm, + )?; + dict.set_item( + "set_eval_frame_func", + vm.ctx + .new_int(RARE_SET_EVAL_FRAME_FUNC.load(Ordering::Relaxed)) + .into(), + vm, + )?; + dict.set_item( + "builtin_dict", + vm.ctx + .new_int(RARE_BUILTIN_DICT.load(Ordering::Relaxed)) + .into(), + vm, + )?; + dict.set_item( + "func_modification", + vm.ctx + .new_int(RARE_FUNC_MODIFICATION.load(Ordering::Relaxed)) + .into(), + vm, + )?; + Ok(dict) + } + + #[pyfunction] + fn set_eval_frame_record(_recorder: PyObjectRef) { + RARE_SET_EVAL_FRAME_FUNC.fetch_add(1, Ordering::Relaxed); + } + + #[pyfunction] + fn set_eval_frame_default() {} + + #[pyfunction] + fn code_returns_only_none(code: PyRef, _vm: &VirtualMachine) -> bool { + crate::vm::crossinterp::code_returns_only_none(&code) + } + + #[pyfunction] + fn get_co_localskinds(code: PyRef, vm: &VirtualMachine) -> PyResult { + let kinds = vm.ctx.new_dict(); + for (offset, &kind) in code.localspluskinds.iter().enumerate() { + kinds.set_item( + code.localsplus_name(offset), + vm.ctx.new_int(kind).into(), + vm, + )?; + } + Ok(kinds) + } + + #[derive(FromArgs)] + struct GetCodeVarCountsArgs { + code: PyObjectRef, + #[pyarg(any, optional)] + globalnames: OptionalArg, + #[pyarg(any, optional)] + attrnames: OptionalArg, + #[pyarg(any, optional)] + globalsns: OptionalArg, + #[pyarg(any, optional)] + builtinsns: OptionalArg, + } + + #[pyfunction] + fn get_code_var_counts(args: GetCodeVarCountsArgs, vm: &VirtualMachine) -> PyResult { + let (code, default_globals, default_builtins) = code_or_function(&args.code, vm)?; + let globalsns = optional_dict(args.globalsns, default_globals, "globalsns", vm)?; + let builtinsns = optional_dict(args.builtinsns, default_builtins, "builtinsns", vm)?; + let mut counts = var_counts(&code); + set_unbound_var_counts( + &code, + &mut counts, + args.globalnames.into_option(), + args.attrnames.into_option(), + globalsns.as_deref(), + builtinsns.as_deref(), + vm, + )?; + counts_to_dict(&counts, vm) + } + + #[derive(FromArgs)] + struct VerifyStatelessCodeArgs { + code: PyObjectRef, + #[pyarg(any, optional)] + globalnames: OptionalArg, + #[pyarg(any, optional)] + globalsns: OptionalArg, + #[pyarg(any, optional)] + builtinsns: OptionalArg, + } + + #[pyfunction] + fn verify_stateless_code(args: VerifyStatelessCodeArgs, vm: &VirtualMachine) -> PyResult<()> { + let (code, default_globals, default_builtins) = code_or_function(&args.code, vm)?; + let globalsns = optional_dict(args.globalsns, default_globals, "globalsns", vm)?; + let builtinsns = optional_dict(args.builtinsns, default_builtins, "builtinsns", vm)?; + let mut counts = var_counts(&code); + set_unbound_var_counts( + &code, + &mut counts, + args.globalnames.into_option(), + None, + globalsns.as_deref(), + builtinsns.as_deref(), + vm, + )?; + if builtinsns.is_some() { + // Force CheckNoExternalState to reject leftover unknown names + // whenever a builtins namespace was supplied. + counts.unbound.globals.numbuiltin += 1; + } + if let Some(errmsg) = check_no_external_state(&counts) { + return Err(vm.new_value_error(errmsg.to_owned())); + } + Ok(()) + } + + #[pyfunction] + fn normalize_path(filename: PyStrRef, _vm: &VirtualMachine) -> String { + normpath(filename.to_str().unwrap_or("")) + } + + #[pyfunction(name = "EncodeLocaleEx")] + fn encode_locale_ex( + text: PyStrRef, + _current_locale: OptionalArg, + errors: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + locale_codec( + text.as_object(), + "encode", + errors.into_option(), + "encode error", + vm, + ) + } + + #[pyfunction(name = "DecodeLocaleEx")] + fn decode_locale_ex( + encoded: PyBytesRef, + _current_locale: OptionalArg, + errors: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + locale_codec( + encoded.as_object(), + "decode", + errors.into_option(), + "decode error", + vm, + ) + } + + #[pyfunction] + fn run_in_subinterp_with_config( + code: PyStrRef, + config: PyObjectRef, + xi: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + if xi.unwrap_or(false) { + return Err(vm.new_runtime_error("cross-interpreter execution is not supported")); + } + let config = crate::stdlib::_interpreters::config_from_pyobject(&config, vm)?; + let source = code + .to_str() + .ok_or_else(|| vm.new_value_error("surrogates not allowed in interpreter source"))?; + #[cfg(feature = "threading")] + { + run_string_in_new_subinterp(source, config, vm) + } + #[cfg(not(feature = "threading"))] + { + let _ = (source, config); + Err(vm.new_runtime_error("isolated interpreters require threading")) + } + } + + #[pyfunction] + fn compiler_cleandoc(doc: PyStrRef) -> String { + clean_doc(doc.to_str().unwrap_or("")) + } + + #[pyfunction] + fn dict_getitem_knownhash( + dict: PyObjectRef, + key: PyObjectRef, + hash: PyHash, + vm: &VirtualMachine, + ) -> PyResult { + let Some(dict) = dict.downcast_ref::() else { + return Err(vm.new_system_error("expected a dict")); + }; + dict.get_item_known_hash(&key, hash, vm)? + .ok_or_else(|| vm.new_key_error(key)) + } + + #[pyfunction] + fn pymem_getallocatorsname() -> &'static str { + if cfg!(debug_assertions) { + "mimalloc_debug" + } else { + "mimalloc" + } + } + + #[pyfunction] + fn get_c_recursion_remaining(vm: &VirtualMachine) -> usize { + vm.recursion_limit + .get() + .saturating_sub(vm.current_recursion_depth()) + } + + #[pyfunction] + fn get_stack_pointer() -> usize { + let marker = 0u8; + core::ptr::from_ref(&marker) as usize + } + + #[pyfunction] + fn get_stack_margin() -> usize { + VirtualMachine::STACK_MARGIN_BYTES + } + + #[pyfunction] + fn get_next_dict_keys_version() -> u32 { + dict_inner::peek_next_keys_version() + } + + #[pyfunction] + fn type_assign_specific_version_unsafe(ty: PyTypeRef, version: u32) { + ty.assign_specific_version(version); + } + + #[pyfunction] + fn get_tracked_heap_size(vm: &VirtualMachine) -> usize { + vm.state.gc.get_objects(None).len() + } + + #[pyfunction] + fn get_long_lived_total(vm: &VirtualMachine) -> usize { + vm.state.gc.get_objects(None).len() + } + + #[pyfunction] + fn get_co_framesize(code: PyRef) -> usize { + code.localspluskinds.len() + code.max_stackdepth as usize + 1 + } + + #[pyfunction] + fn iframe_getcode(frame: PyRef) -> PyRef { + frame.f_code() + } + + #[pyfunction] + fn iframe_getline(frame: PyRef) -> usize { + frame.f_lineno() + } + + #[pyfunction] + fn iframe_getlasti(frame: PyObjectRef, vm: &VirtualMachine) -> PyResult { + frame.get_attr("f_lasti", vm) + } + + #[pyfunction] + fn get_static_builtin_types(vm: &VirtualMachine) -> Vec { + let mut out = Vec::new(); + let mut seen = std::collections::HashSet::new(); + let mut stack = vec![vm.ctx.types.object_type.to_owned()]; + while let Some(cls) = stack.pop() { + let ptr = cls.as_object() as *const PyObject as usize; + if !seen.insert(ptr) { + continue; + } + if cls.slots.flags.has_feature(PyTypeFlags::HEAPTYPE) { + continue; + } + out.push(cls.clone().into()); + for weak in cls.subclasses.read().iter() { + if let Some(sub) = weak.upgrade() + && let Ok(sub) = sub.downcast::() + { + stack.push(sub); + } + } + } + out + } + + #[pyfunction] + fn identify_type_slot_wrappers(vm: &VirtualMachine) -> Vec { + let object = vm.ctx.types.object_type; + object + .get_attributes(&vm.ctx) + .into_iter() + .filter(|(_, value)| value.downcastable::()) + .map(|(name, _)| name.to_owned().into()) + .collect() + } + + #[pyfunction] + fn get_configs(vm: &VirtualMachine) -> PyResult { + let settings = &vm.state.config.settings; + let global = vm.ctx.new_dict(); + global.set_item("isolated", vm.ctx.new_bool(settings.isolated).into(), vm)?; + global.set_item("dev_mode", vm.ctx.new_bool(settings.dev_mode).into(), vm)?; + global.set_item("verbose", vm.ctx.new_int(settings.verbose).into(), vm)?; + global.set_item("quiet", vm.ctx.new_bool(settings.quiet).into(), vm)?; + global.set_item("inspect", vm.ctx.new_bool(settings.inspect).into(), vm)?; + global.set_item( + "interactive", + vm.ctx.new_bool(settings.interactive).into(), + vm, + )?; + global.set_item( + "optimize", + vm.ctx.new_int(settings.optimize as i32).into(), + vm, + )?; + global.set_item( + "hash_seed", + vm.ctx.new_int(settings.hash_seed.unwrap_or(0)).into(), + vm, + )?; + let configs = vm.ctx.new_dict(); + configs.set_item("config", global.into(), vm)?; + Ok(configs) + } + + #[pyfunction] + fn get_interp_settings(id: OptionalArg, vm: &VirtualMachine) -> PyResult { + let _ = id; + let flags = vm.state.feature_flags; + let settings = vm.ctx.new_dict(); + let mut bits = 0u64; + if flags.use_main_obmalloc { + bits |= 1 << 5; + } + if flags.check_multi_interp_extensions { + bits |= 1 << 8; + } + settings.set_item("feature_flags", vm.ctx.new_int(bits).into(), vm)?; + settings.set_item("own_gil", vm.ctx.new_bool(true).into(), vm)?; + Ok(settings) + } + + #[pyfunction] + fn create_interpreter( + config: OptionalArg, + _whence: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + let cfg = match config.into_option() { + Some(obj) if !vm.is_none(&obj) => { + crate::stdlib::_interpreters::config_from_pyobject(&obj, vm)? + } + _ => crate::vm::InterpreterConfig::ISOLATED, + }; + #[cfg(feature = "threading")] + { + let interp = crate::Interpreter::create_subinterpreter_from_vm(vm, cfg) + .map_err(|msg| crate::stdlib::_interpreters::interpreter_error(vm, msg))?; + Ok(crate::vm::runtime::store_owned_interpreter(interp)) + } + #[cfg(not(feature = "threading"))] + { + let _ = cfg; + Err(vm.new_runtime_error("isolated interpreters require threading")) + } + } + + #[pyfunction(name = "_PyTime_AsTimespec")] + fn pytime_as_timespec(ns: i64) -> (i64, i64) { + let sec = ns.div_euclid(1_000_000_000); + let nsec = ns.rem_euclid(1_000_000_000); + (sec, nsec) + } + + #[pyfunction(name = "_PyTime_AsTimespec_clamp")] + fn pytime_as_timespec_clamp(ns: i64) -> (i64, i64) { + pytime_as_timespec(ns) + } + + #[pyfunction(name = "_PyTime_AsTimeval_clamp")] + fn pytime_as_timeval_clamp(ns: i64, _rnd: OptionalArg) -> (i64, i64) { + let us = ns.div_euclid(1000); + let sec = us.div_euclid(1_000_000); + let usec = us.rem_euclid(1_000_000); + (sec, usec) + } + + #[pyfunction] + fn test_edit_cost() {} + + #[pyfunction] + fn hamt(vm: &VirtualMachine) -> PyRef { + Hamt::default().into_ref(&vm.ctx) + } + + #[pyclass( + no_attr, + name = "hamt", + module = "_testinternalcapi", + traverse = "manual" + )] + #[derive(Default, Debug, PyPayload)] + struct Hamt { + buckets: HashMap>, + len: usize, + } + + // SAFETY: each stored key/value is visited at most once. + unsafe impl Traverse for Hamt { + fn traverse(&self, traverse_fn: &mut TraverseFn<'_>) { + // Visit order is irrelevant for GC tracing. + #[allow(clippy::iter_over_hash_type)] + for pairs in self.buckets.values() { + for (k, v) in pairs { + k.traverse(traverse_fn); + v.traverse(traverse_fn); + } + } + } + + fn clear(&mut self, out: &mut Vec) { + #[allow(clippy::iter_over_hash_type)] + for (_, pairs) in self.buckets.drain() { + for (k, v) in pairs { + out.push(k); + out.push(v); + } + } + self.len = 0; + } + } + + #[derive(Clone, Copy, Debug)] + enum HamtIterKind { + Keys, + Values, + Items, + } + + #[pyclass( + no_attr, + name = "hamt_iterator", + module = "_testinternalcapi", + traverse + )] + #[derive(Debug, PyPayload)] + struct HamtIter { + items: Vec<(PyObjectRef, PyObjectRef)>, + #[pytraverse(skip)] + kind: HamtIterKind, + #[pytraverse(skip)] + pos: AtomicUsize, + } + + #[pyclass(flags(HAS_WEAKREF), with(AsMapping, AsSequence, Comparable, Iterable))] + impl Hamt { + fn find(&self, key: &PyObject, vm: &VirtualMachine) -> PyResult> { + let hash = key.hash(vm)?; + if let Some(bucket) = self.buckets.get(&hash) { + for (k, v) in bucket { + if vm.bool_eq(k, key)? { + return Ok(Some(v.clone())); + } + } + } + Ok(None) + } + + fn pairs(&self) -> impl Iterator { + self.buckets.values().flatten() + } + + fn snapshot(&self) -> Vec<(PyObjectRef, PyObjectRef)> { + self.pairs().cloned().collect() + } + + fn iter_with_kind(&self, kind: HamtIterKind, vm: &VirtualMachine) -> PyRef { + HamtIter { + items: self.snapshot(), + kind, + pos: AtomicUsize::new(0), + } + .into_ref(&vm.ctx) + } + + fn clone_map(&self) -> Self { + Self { + buckets: self.buckets.clone(), + len: self.len, + } + } + + #[pymethod] + fn set( + zelf: PyRef, + key: PyObjectRef, + value: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult> { + let hash = key.hash(vm)?; + if let Some(bucket) = zelf.buckets.get(&hash) { + for (k, slot) in bucket { + if vm.bool_eq(k, &key)? { + if slot.is(&value) { + return Ok(zelf); + } + let mut next = zelf.clone_map(); + let next_bucket = next.buckets.get_mut(&hash).unwrap(); + for (nk, nv) in next_bucket.iter_mut() { + if vm.bool_eq(nk, &key)? { + *nv = value; + break; + } + } + return Ok(next.into_ref(&vm.ctx)); + } + } + } + let mut next = zelf.clone_map(); + next.buckets.entry(hash).or_default().push((key, value)); + next.len += 1; + Ok(next.into_ref(&vm.ctx)) + } + + #[pymethod] + fn get( + &self, + key: PyObjectRef, + default: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + match self.find(&key, vm)? { + Some(value) => Ok(value), + None => Ok(default.unwrap_or_none(vm)), + } + } + + #[pymethod] + fn delete( + zelf: PyRef, + key: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult> { + let hash = key.hash(vm)?; + let Some(bucket) = zelf.buckets.get(&hash) else { + return Ok(zelf); + }; + let mut idx = None; + for (i, (k, _)) in bucket.iter().enumerate() { + if vm.bool_eq(k, &key)? { + idx = Some(i); + break; + } + } + let Some(i) = idx else { + return Ok(zelf); + }; + let mut next = zelf.clone_map(); + let next_bucket = next.buckets.get_mut(&hash).unwrap(); + next_bucket.remove(i); + next.len -= 1; + if next_bucket.is_empty() { + next.buckets.remove(&hash); + } + Ok(next.into_ref(&vm.ctx)) + } + + #[pymethod] + fn keys(&self, vm: &VirtualMachine) -> PyRef { + self.iter_with_kind(HamtIterKind::Keys, vm) + } + + #[pymethod] + fn values(&self, vm: &VirtualMachine) -> PyRef { + self.iter_with_kind(HamtIterKind::Values, vm) + } + + #[pymethod] + fn items(&self, vm: &VirtualMachine) -> PyRef { + self.iter_with_kind(HamtIterKind::Items, vm) + } + } + + impl AsMapping for Hamt { + fn as_mapping() -> &'static PyMappingMethods { + static METHODS: PyMappingMethods = PyMappingMethods { + length: atomic_func!(|mapping, _vm| Ok(Hamt::mapping_downcast(mapping).len)), + subscript: atomic_func!(|mapping, needle, vm| { + Hamt::mapping_downcast(mapping) + .find(needle, vm)? + .ok_or_else(|| vm.new_key_error(needle.to_owned())) + }), + ass_subscript: None, + }; + &METHODS + } + } + + impl AsSequence for Hamt { + fn as_sequence() -> &'static PySequenceMethods { + static AS_SEQUENCE: LazyLock = LazyLock::new(|| PySequenceMethods { + contains: atomic_func!(|seq, target, vm| { + Ok(Hamt::sequence_downcast(seq).find(target, vm)?.is_some()) + }), + ..PySequenceMethods::NOT_IMPLEMENTED + }); + &AS_SEQUENCE + } + } + + impl Comparable for Hamt { + fn cmp( + zelf: &Py, + other: &PyObject, + op: PyComparisonOp, + vm: &VirtualMachine, + ) -> PyResult { + let other = class_or_notimplemented!(Self, other); + op.eq_only(|| Ok(hamt_eq(zelf, other, vm)?.into())) + } + } + + fn hamt_eq(left: &Py, right: &Py, vm: &VirtualMachine) -> PyResult { + if left.is(right) { + return Ok(true); + } + if left.len != right.len { + return Ok(false); + } + let pairs = left.snapshot(); + for (key, left_value) in pairs { + match right.find(&key, vm)? { + Some(right_value) => { + if !vm.bool_eq(&left_value, &right_value)? { + return Ok(false); + } + } + None => return Ok(false), + } + } + Ok(true) + } + + impl Iterable for Hamt { + fn iter(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + Ok(zelf.keys(vm).into()) + } + } + + #[pyclass(with(IterNext, Iterable, AsMapping))] + impl HamtIter {} + + impl SelfIter for HamtIter {} + + impl IterNext for HamtIter { + fn next(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let pos = zelf.pos.fetch_add(1, Ordering::Relaxed); + let Some((key, value)) = zelf.items.get(pos) else { + zelf.pos.fetch_sub(1, Ordering::Relaxed); + return Ok(PyIterReturn::StopIteration(None)); + }; + let item = match zelf.kind { + HamtIterKind::Keys => key.clone(), + HamtIterKind::Values => value.clone(), + HamtIterKind::Items => vm.ctx.new_tuple(vec![key.clone(), value.clone()]).into(), + }; + Ok(PyIterReturn::Return(item)) + } + } + + impl AsMapping for HamtIter { + fn as_mapping() -> &'static PyMappingMethods { + static METHODS: PyMappingMethods = PyMappingMethods { + length: atomic_func!(|mapping, _vm| { + Ok(HamtIter::mapping_downcast(mapping).items.len()) + }), + subscript: None, + ass_subscript: None, + }; + &METHODS + } + } + + #[cfg(feature = "codegen")] + #[pyfunction] + fn new_instruction_sequence(vm: &VirtualMachine) -> PyRef { + PyInstructionSequence::empty().into_ref(&vm.ctx) + } + + #[cfg(feature = "codegen")] + #[pyfunction] + fn compiler_codegen( + ast: PyObjectRef, + filename: PyObjectRef, + optimize: i32, + compile_mode: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult { + let filename = super::filename_to_string(&filename, vm)?; + let converted = crate::stdlib::_ast::rust_mod_from_object(vm, ast, &filename)?; + let mode = match compile_mode.unwrap_or(0) { + 1 => crate::compiler::Mode::Eval, + 2 => crate::compiler::Mode::Single, + _ => crate::compiler::Mode::Exec, + }; + let mut opts = rustpython_codegen::CompileOpts { + optimize: u8::try_from(optimize).unwrap_or(0), + ..rustpython_codegen::CompileOpts::default() + }; + opts.future_features |= rustpython_codegen::preprocess::future_features(&converted.ast); + let output = rustpython_codegen::compile::compile_codegen( + converted.ast, + converted.source_file, + mode, + opts, + ) + .map_err(|err| vm.new_syntax_error(&crate::compiler::CompileError::from(err), None))?; + let seq = PyInstructionSequence::from_rust(output.seq, vm); + let metadata = vm.ctx.new_dict(); + metadata.set_item("argcount", vm.ctx.new_int(output.argcount).into(), vm)?; + metadata.set_item( + "posonlyargcount", + vm.ctx.new_int(output.posonlyargcount).into(), + vm, + )?; + metadata.set_item( + "kwonlyargcount", + vm.ctx.new_int(output.kwonlyargcount).into(), + vm, + )?; + let consts: Vec = output + .consts + .into_iter() + .map(|c| super::constant_data_to_py(c, vm)) + .collect(); + metadata.set_item("consts", vm.ctx.new_list(consts).into(), vm)?; + Ok(vm.ctx.new_tuple(vec![seq.into(), metadata.into()]).into()) + } + + #[cfg(feature = "codegen")] + #[pyfunction] + fn optimize_cfg( + instructions: PyObjectRef, + consts: PyObjectRef, + nlocals: i32, + vm: &VirtualMachine, + ) -> PyResult> { + let seq = instructions + .downcast::() + .map_err(|_| vm.new_value_error("expected an instruction sequence"))?; + let consts_list = consts + .downcast::() + .map_err(|_| vm.new_type_error("consts must be a list"))?; + let nlocals = usize::try_from(nlocals).unwrap_or(0); + let rust_seq = { + let mut inner = seq.seq.lock(); + inner.apply_label_map(); + inner.clone() + }; + let mut const_data = Vec::new(); + for item in consts_list.borrow_vec().iter() { + const_data.push(super::py_to_constant_data(item.clone(), vm)?); + } + let (optimized, new_consts) = + rustpython_codegen::ir::optimize_cfg_for_tests(rust_seq, const_data, nlocals) + .map_err(|err| super::internal_error_to_py(err, vm))?; + { + let mut items = consts_list.borrow_vec_mut(); + items.clear(); + for constant in new_consts { + items.push(super::constant_data_to_py(constant, vm)); + } + } + Ok(PyInstructionSequence::from_rust(optimized, vm)) + } + + #[cfg(feature = "codegen")] + #[pyfunction] + fn assemble_code_object( + filename: PyObjectRef, + instructions: PyObjectRef, + metadata: PyObjectRef, + vm: &VirtualMachine, + ) -> PyResult> { + let seq = instructions + .downcast::() + .map_err(|_| vm.new_type_error("expected an instruction sequence"))?; + let metadata = metadata + .downcast::() + .map_err(|_| vm.new_type_error("metadata must be a dict"))?; + let filename = super::filename_to_string(&filename, vm)?; + let rust_seq = { + let mut inner = seq.seq.lock(); + inner.apply_label_map(); + inner.clone() + }; + let unit = super::metadata_to_code_unit(&metadata, vm)?; + let code = rustpython_codegen::ir::assemble_for_tests(filename, rust_seq, unit, true) + .map_err(|err| super::internal_error_to_py(err, vm))?; + Ok(vm.ctx.new_code(code)) + } + + #[cfg(feature = "codegen")] + #[pyclass(no_attr, name = "InstructionSequence", module = "_testinternalcapi")] + #[derive(PyPayload)] + struct PyInstructionSequence { + seq: rustpython_common::lock::PyMutex, + nested: rustpython_common::lock::PyMutex>>, + } + + #[cfg(feature = "codegen")] + impl core::fmt::Debug for PyInstructionSequence { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("InstructionSequence") + } + } + + #[cfg(feature = "codegen")] + #[pyclass] + impl PyInstructionSequence { + #[pymethod] + #[allow(clippy::too_many_arguments)] + fn addop( + &self, + opcode: i32, + oparg: i32, + lineno: i32, + col_offset: i32, + end_lineno: i32, + end_col_offset: i32, + vm: &VirtualMachine, + ) -> PyResult<()> { + self.seq + .lock() + .addop( + opcode, + oparg, + lineno, + col_offset, + end_lineno, + end_col_offset, + ) + .map_err(|err| super::internal_error_to_py(err, vm)) + } + + #[pymethod] + fn use_label(&self, label: i32, vm: &VirtualMachine) -> PyResult<()> { + self.seq + .lock() + .use_label(label) + .map_err(|err| super::internal_error_to_py(err, vm)) + } + + #[pymethod] + fn new_label(&self) -> i32 { + self.seq.lock().new_label() + } + + #[pymethod] + fn add_nested(&self, nested: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + let nested = nested.downcast::().map_err(|obj| { + vm.new_type_error(format!( + "expected an instruction sequence, not {}", + obj.class().name() + )) + })?; + self.nested.lock().push(nested); + Ok(()) + } + + #[pymethod] + fn get_nested(&self, vm: &VirtualMachine) -> crate::builtins::PyListRef { + vm.ctx.new_list( + self.nested + .lock() + .iter() + .map(|seq| seq.clone().into()) + .collect(), + ) + } + + #[pymethod] + fn get_instructions(&self, vm: &VirtualMachine) -> crate::builtins::PyListRef { + let mut seq = self.seq.lock(); + seq.apply_label_map(); + let items = seq + .python_instructions() + .into_iter() + .map(|instr| super::python_instruction_to_tuple(instr, vm)) + .collect(); + vm.ctx.new_list(items) + } + } + + #[cfg(feature = "codegen")] + impl PyInstructionSequence { + fn empty() -> Self { + Self { + seq: rustpython_common::lock::PyMutex::new( + rustpython_codegen::ir::InstructionSequence::new(), + ), + nested: rustpython_common::lock::PyMutex::new(Vec::new()), + } + } + + fn from_rust( + mut seq: rustpython_codegen::ir::InstructionSequence, + vm: &VirtualMachine, + ) -> PyRef { + let nested = seq + .nested_mut() + .drain(..) + .map(|child| Self::from_rust(child, vm)) + .collect(); + Self { + seq: rustpython_common::lock::PyMutex::new(seq), + nested: rustpython_common::lock::PyMutex::new(nested), + } + .into_ref(&vm.ctx) + } + } +} + +use crate::{ + AsObject, Py, PyObject, PyObjectRef, PyRef, PyResult, VirtualMachine, + builtins::{PyCode, PyDict, PyDictRef, PyFunction, PySet, PyStr, PyStrInterned, PyStrRef}, + bytecode::{ + CO_FAST_ARG, CO_FAST_ARG_KW, CO_FAST_ARG_POS, CO_FAST_ARG_VAR, CO_FAST_CELL, CO_FAST_FREE, + CO_FAST_HIDDEN, Instruction, + }, + convert::ToPyObject, + function::OptionalArg, +}; +use std::collections::HashSet; + +fn clean_doc(doc: &str) -> String { + let doc = expandtabs(doc, 8); + let margin = doc + .split('\n') + .skip(1) + .filter(|line| line.chars().any(|c| c != ' ')) + .map(|line| line.chars().take_while(|c| *c == ' ').count()) + .min() + .unwrap_or(0); + + let mut cleaned = String::with_capacity(doc.len()); + if let Some(first_line) = doc.split('\n').next() { + let trimmed = first_line.trim_start(); + if trimmed.len() == first_line.len() && margin == 0 { + return doc.to_owned(); + } + cleaned.push_str(trimmed); + } + 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 +} + +fn expandtabs(input: &str, tab_size: usize) -> String { + let mut expanded = String::with_capacity(input.len()); + let mut col = 0usize; + let mut next_stop = tab_size; + for ch in input.chars() { + match ch { + '\t' => { + let n = next_stop - col; + col += n; + expanded.extend(core::iter::repeat_n(' ', n)); + } + '\r' | '\n' => { + expanded.push(ch); + col = 0; + next_stop = 0; + } + _ => { + expanded.push(ch); + col += 1; + } + } + if col >= next_stop { + next_stop += tab_size; + } + } + expanded +} + +type CodeNamespaces = (PyRef, Option>, Option>); + +fn code_or_function(obj: &PyObject, vm: &VirtualMachine) -> PyResult { + if let Ok(func) = obj.to_owned().downcast::() { + let builtins = func.builtins.clone().downcast::().ok(); + return Ok(( + (*func.code).to_owned(), + Some(func.globals.clone()), + builtins, + )); + } + if let Ok(code) = obj.to_owned().downcast::() { + return Ok((code, None, None)); + } + Err(vm.new_type_error("argument must be a code object or a function")) +} + +fn optional_dict( + override_ns: OptionalArg, + default: Option>, + name: &str, + vm: &VirtualMachine, +) -> PyResult>> { + match override_ns.into_option() { + Some(obj) => obj.downcast::().map(Some).map_err(|obj| { + vm.new_type_error(format!( + "expected a dict for \"{name}\", got {}", + obj.class().name() + )) + }), + None => Ok(default), + } +} + +#[derive(Default, Clone, Copy)] +struct ArgsCounts { + total: i32, + numposonly: i32, + numposorkw: i32, + numkwonly: i32, + varargs: i32, + varkwargs: i32, +} + +#[derive(Default, Clone, Copy)] +struct CellCounts { + total: i32, + numargs: i32, + numothers: i32, +} + +#[derive(Default, Clone, Copy)] +struct HiddenCounts { + total: i32, + numpure: i32, + numcells: i32, +} + +#[derive(Default, Clone, Copy)] +struct LocalsCounts { + total: i32, + args: ArgsCounts, + numpure: i32, + cells: CellCounts, + hidden: HiddenCounts, +} + +#[derive(Default, Clone, Copy)] +struct GlobalCounts { + total: i32, + numglobal: i32, + numbuiltin: i32, + numunknown: i32, +} + +#[derive(Default, Clone, Copy)] +struct UnboundCounts { + total: i32, + globals: GlobalCounts, + numattrs: i32, + numunknown: i32, +} + +#[derive(Default, Clone, Copy)] +struct VarCounts { + total: i32, + locals: LocalsCounts, + numfree: i32, + unbound: UnboundCounts, +} + +fn var_counts(code: &PyCode) -> VarCounts { + let mut locals = LocalsCounts::default(); + let mut numfree = 0; + for &kind in &code.localspluskinds { + if kind & CO_FAST_FREE != 0 { + numfree += 1; + } else { + locals.total += 1; + if kind & CO_FAST_ARG != 0 { + locals.args.total += 1; + if kind & CO_FAST_ARG_VAR != 0 { + if kind & CO_FAST_ARG_POS != 0 { + locals.args.varargs = 1; + } else { + locals.args.varkwargs = 1; + } + } else if kind & CO_FAST_ARG_POS != 0 { + if kind & CO_FAST_ARG_KW != 0 { + locals.args.numposorkw += 1; + } else { + locals.args.numposonly += 1; + } + } else { + locals.args.numkwonly += 1; + } + if kind & CO_FAST_CELL != 0 { + locals.cells.total += 1; + locals.cells.numargs += 1; + } + } else if kind & CO_FAST_CELL != 0 { + locals.cells.total += 1; + locals.cells.numothers += 1; + if kind & CO_FAST_HIDDEN != 0 { + locals.hidden.total += 1; + locals.hidden.numcells += 1; + } + } else { + locals.numpure += 1; + if kind & CO_FAST_HIDDEN != 0 { + locals.hidden.total += 1; + locals.hidden.numpure += 1; + } + } + } + } + let numunbound = code.names.len() as i32; + let unbound = UnboundCounts { + total: numunbound, + numunknown: numunbound, + ..UnboundCounts::default() + }; + VarCounts { + total: locals.total + numfree + unbound.total, + locals, + numfree, + unbound, + } +} + +fn set_unbound_var_counts( + code: &PyCode, + counts: &mut VarCounts, + globalnames: Option, + attrnames: Option, + globalsns: Option<&Py>, + builtinsns: Option<&Py>, + vm: &VirtualMachine, +) -> PyResult<()> { + let globalnames = optional_set(globalnames, "globalnames", vm)?; + let attrnames = optional_set(attrnames, "attrnames", vm)?; + let (unbound, numdupes) = + identify_unbound_names(code, globalnames, attrnames, globalsns, builtinsns, vm)?; + let totalunbound = counts.unbound.total + numdupes; + let mut unbound = unbound; + unbound.numunknown = totalunbound - unbound.total; + unbound.total = totalunbound; + counts.unbound = unbound; + counts.total += numdupes; + Ok(()) +} + +fn optional_set( + obj: Option, + name: &str, + vm: &VirtualMachine, +) -> PyResult>> { + match obj { + None => Ok(None), + Some(obj) => obj.downcast::().map(Some).map_err(|obj| { + vm.new_type_error(format!( + "expected a set for \"{name}\", got {}", + obj.class().name() + )) + }), + } +} + +fn identify_unbound_names( + code: &PyCode, + globalnames: Option>, + attrnames: Option>, + globalsns: Option<&Py>, + builtinsns: Option<&Py>, + vm: &VirtualMachine, +) -> PyResult<(UnboundCounts, i32)> { + let mut seen_globals = HashSet::new(); + let mut seen_attrs = HashSet::new(); + let mut unbound = UnboundCounts::default(); + let mut numdupes = 0; + for (_, instr, arg) in crate::vm::crossinterp::walk_instructions(code) { + match instr { + Instruction::LoadAttr { namei } => { + let name = code.names[namei.get(arg).name_idx() as usize]; + if name_already_seen(&seen_attrs, attrnames.as_deref(), name, vm)? { + continue; + } + seen_attrs.insert(name_key(name)); + if let Some(set) = attrnames.as_deref() { + set.add(name.to_owned().into(), vm)?; + } + unbound.total += 1; + unbound.numattrs += 1; + if name_already_seen(&seen_globals, globalnames.as_deref(), name, vm)? { + numdupes += 1; + } + } + Instruction::LoadGlobal { namei } => { + let name = code.names[(namei.get(arg) >> 1) as usize]; + if name_already_seen(&seen_globals, globalnames.as_deref(), name, vm)? { + continue; + } + seen_globals.insert(name_key(name)); + if let Some(set) = globalnames.as_deref() { + set.add(name.to_owned().into(), vm)?; + } + unbound.total += 1; + unbound.globals.total += 1; + if globalsns.is_some_and(|ns| ns.contains_key(name, vm)) { + unbound.globals.numglobal += 1; + } else if builtinsns.is_some_and(|ns| ns.contains_key(name, vm)) { + unbound.globals.numbuiltin += 1; + } else { + unbound.globals.numunknown += 1; + } + if name_already_seen(&seen_attrs, attrnames.as_deref(), name, vm)? { + numdupes += 1; + } + } + _ => {} + } + } + Ok((unbound, numdupes)) +} + +fn name_already_seen( + seen: &HashSet, + user_set: Option<&Py>, + name: &'static PyStrInterned, + vm: &VirtualMachine, +) -> PyResult { + if seen.contains(&name_key(name)) { + return Ok(true); + } + if let Some(set) = user_set { + return set.__contains__(name.as_object(), vm); + } + Ok(false) +} + +fn name_key(name: &'static PyStrInterned) -> usize { + core::ptr::from_ref(name.as_object()) as usize +} + +fn check_no_external_state(counts: &VarCounts) -> Option<&'static str> { + if counts.numfree > 0 { + Some("closures not supported") + } else if counts.unbound.globals.numglobal > 0 + || (counts.unbound.globals.numbuiltin > 0 && counts.unbound.globals.numunknown > 0) + { + Some("globals not supported") + } else { + None + } +} + +fn counts_to_dict(counts: &VarCounts, vm: &VirtualMachine) -> PyResult { + let countsobj = vm.ctx.new_dict(); + set_count(&countsobj, "total", counts.total, vm)?; + + let locals = vm.ctx.new_dict(); + countsobj.set_item("locals", locals.clone().into(), vm)?; + set_count(&locals, "total", counts.locals.total, vm)?; + + let args = vm.ctx.new_dict(); + locals.set_item("args", args.clone().into(), vm)?; + set_count(&args, "total", counts.locals.args.total, vm)?; + set_count(&args, "numposonly", counts.locals.args.numposonly, vm)?; + set_count(&args, "numposorkw", counts.locals.args.numposorkw, vm)?; + set_count(&args, "numkwonly", counts.locals.args.numkwonly, vm)?; + set_count(&args, "varargs", counts.locals.args.varargs, vm)?; + set_count(&args, "varkwargs", counts.locals.args.varkwargs, vm)?; + + set_count(&locals, "numpure", counts.locals.numpure, vm)?; + + let cells = vm.ctx.new_dict(); + locals.set_item("cells", cells.clone().into(), vm)?; + set_count(&cells, "total", counts.locals.cells.total, vm)?; + set_count(&cells, "numargs", counts.locals.cells.numargs, vm)?; + set_count(&cells, "numothers", counts.locals.cells.numothers, vm)?; + + let hidden = vm.ctx.new_dict(); + locals.set_item("hidden", hidden.clone().into(), vm)?; + set_count(&hidden, "total", counts.locals.hidden.total, vm)?; + set_count(&hidden, "numpure", counts.locals.hidden.numpure, vm)?; + set_count(&hidden, "numcells", counts.locals.hidden.numcells, vm)?; + + set_count(&countsobj, "numfree", counts.numfree, vm)?; + + let unbound = vm.ctx.new_dict(); + countsobj.set_item("unbound", unbound.clone().into(), vm)?; + set_count(&unbound, "total", counts.unbound.total, vm)?; + set_count(&unbound, "numattrs", counts.unbound.numattrs, vm)?; + set_count(&unbound, "numunknown", counts.unbound.numunknown, vm)?; + + let globals = vm.ctx.new_dict(); + unbound.set_item("globals", globals.clone().into(), vm)?; + set_count(&globals, "total", counts.unbound.globals.total, vm)?; + set_count(&globals, "numglobal", counts.unbound.globals.numglobal, vm)?; + set_count( + &globals, + "numbuiltin", + counts.unbound.globals.numbuiltin, + vm, + )?; + set_count( + &globals, + "numunknown", + counts.unbound.globals.numunknown, + vm, + )?; + + Ok(countsobj) +} + +fn set_count(dict: &Py, name: &str, value: i32, vm: &VirtualMachine) -> PyResult<()> { + dict.set_item(name, vm.ctx.new_int(value).into(), vm) +} + +fn locale_error_handler(errors: Option, vm: &VirtualMachine) -> PyResult { + let Some(errors) = errors else { + return Ok(vm.ctx.new_str("strict")); + }; + match errors.to_str() { + Some("strict" | "surrogateescape" | "surrogatepass") => Ok(errors), + _ => Err(vm.new_value_error("unsupported error handler")), + } +} + +fn locale_codec( + obj: &PyObject, + method: &str, + errors: Option, + kind: &str, + vm: &VirtualMachine, +) -> PyResult { + let errors = locale_error_handler(errors, vm)?; + let encoding: PyObjectRef = vm.fs_encoding().to_owned().into(); + match vm.call_method(obj, method, (encoding, errors)) { + Ok(value) => Ok(value), + Err(exc) => { + if exc.fast_isinstance(vm.ctx.exceptions.lookup_error) { + return Err(vm.new_value_error("unsupported error handler")); + } + let codec_exc = if method == "encode" { + vm.ctx.exceptions.unicode_encode_error + } else { + vm.ctx.exceptions.unicode_decode_error + }; + if exc.fast_isinstance(codec_exc) { + let start = exc + .as_object() + .get_attr("start", vm) + .ok() + .and_then(|v| v.try_to_value::(vm).ok()) + .unwrap_or(0); + let reason = exc + .as_object() + .get_attr("reason", vm) + .ok() + .and_then(|v| { + v.downcast_ref::() + .and_then(|s| s.to_str()) + .map(str::to_owned) + }) + .unwrap_or_default(); + return Err(vm.new_runtime_error(format!("{kind}: pos={start}, reason={reason}"))); + } + Err(exc) + } + } +} + +fn normpath(path: &str) -> String { + if path.is_empty() { + return String::new(); + } + let mut buf: Vec = path.chars().collect(); + let size = buf.len(); + let (drvsize, rootsize) = skiproot(&buf); + let mut p1 = drvsize + rootsize; + let mut p2 = p1; + let min_p2 = p2.saturating_sub(1); + let mut last_c = if drvsize + rootsize > 0 { + buf[min_p2] + } else { + '\0' + }; + + if p1 < size && buf[p1] == '.' && sep_or_end(&buf, p1 + 1, size) { + p1 += 1; + last_c = if p1 < size { buf[p1] } else { '\0' }; + while p1 < size && buf[p1] == '/' { + p1 += 1; + } + } + + while p1 < size { + let c = buf[p1]; + if last_c == '/' { + if c == '.' { + let sep_at_1 = sep_or_end(&buf, p1 + 1, size); + let sep_at_2 = !sep_at_1 && sep_or_end(&buf, p1 + 2, size); + if sep_at_2 && p1 + 1 < size && buf[p1 + 1] == '.' { + let mut p3 = p2; + while p3 != min_p2 && { + p3 -= 1; + buf[p3] == '/' + } {} + while p3 != min_p2 && buf[p3 - 1] != '/' { + p3 -= 1; + } + if p2 == min_p2 + || (buf.get(p3) == Some(&'.') + && buf.get(p3 + 1) == Some(&'.') + && (p3 + 2 >= size || buf[p3 + 2] == '/')) + { + buf[p2] = '.'; + p2 += 1; + buf[p2] = '.'; + p2 += 1; + last_c = '.'; + } else if buf.get(p3) == Some(&'/') { + p2 = p3 + 1; + } else { + p2 = p3; + } + p1 += 1; + } else if sep_at_1 { + } else { + buf[p2] = c; + p2 += 1; + last_c = c; + } + } else if c != '/' { + buf[p2] = c; + p2 += 1; + last_c = c; + } + } else { + buf[p2] = c; + p2 += 1; + last_c = c; + } + p1 += 1; + } + + if p2 != min_p2 { + while p2 != 0 { + p2 -= 1; + if p2 == min_p2 || buf[p2] != '/' { + break; + } + } + buf.truncate(p2 + 1); + } else if p2 > 0 { + buf.truncate(p2); + } else { + buf.clear(); + } + buf.into_iter().collect() +} + +fn skiproot(path: &[char]) -> (usize, usize) { + if path.first() != Some(&'/') { + return (0, 0); + } + let c1 = path.get(1).copied().unwrap_or('\0'); + let c2 = path.get(2).copied().unwrap_or('\0'); + if c1 != '/' || c2 == '/' { + (0, 1) + } else { + (0, 2) + } +} + +fn sep_or_end(path: &[char], idx: usize, size: usize) -> bool { + idx >= size || path[idx] == '/' +} + +#[cfg(feature = "threading")] +fn run_string_in_new_subinterp( + source: &str, + config: crate::vm::InterpreterConfig, + vm: &VirtualMachine, +) -> PyResult { + let interp = crate::Interpreter::create_subinterpreter_from_vm(vm, config).map_err(|msg| { + let cause = vm.new_runtime_error(msg.to_owned()); + let exc = + crate::stdlib::_interpreters::interpreter_error(vm, "sub-interpreter creation failed"); + exc.set___context__(Some(cause)); + exc + })?; + let id = crate::vm::runtime::store_owned_interpreter(interp); + let outcome = crate::vm::crossinterp::with_interpreter(id, vm, |target| { + match target.compile(source, crate::compiler::Mode::Exec, "") { + Ok(code) => { + let ns = target.main_namespace()?; + let scope = crate::scope::Scope::with_builtins(None, ns, target); + match target.run_code_obj(code, scope) { + Ok(_) => Ok(0), + Err(exc) => { + target.print_exception(exc); + Ok(-1) + } + } + } + Err(err) => { + let exc = err.into_pyexception(target, Some(source)); + target.print_exception(exc); + Ok(-1) + } + } + }); + let _ = crate::vm::runtime::destroy_owned_interpreter(id); + outcome +} + +#[cfg(feature = "codegen")] +fn python_instruction_to_tuple( + instr: rustpython_codegen::ir::PythonInstruction, + vm: &VirtualMachine, +) -> PyObjectRef { + let oparg = match instr.oparg { + Some(arg) => vm.ctx.new_int(arg).into(), + None => vm.ctx.none(), + }; + vm.ctx + .new_tuple(vec![ + vm.ctx.new_int(instr.opcode).into(), + oparg, + vm.ctx.new_int(instr.lineno).into(), + vm.ctx.new_int(instr.end_lineno).into(), + vm.ctx.new_int(instr.col_offset).into(), + vm.ctx.new_int(instr.end_col_offset).into(), + ]) + .into() +} + +#[cfg(feature = "codegen")] +fn filename_to_string(filename: &PyObject, vm: &VirtualMachine) -> PyResult { + if let Some(s) = filename.downcast_ref::() { + return Ok(s.to_string_lossy().into_owned()); + } + filename.str(vm).map(|s| s.to_string_lossy().into_owned()) +} + +#[cfg(feature = "codegen")] +fn internal_error_to_py( + err: rustpython_codegen::error::InternalError, + vm: &VirtualMachine, +) -> crate::builtins::PyBaseExceptionRef { + match err { + rustpython_codegen::error::InternalError::ConstIndexOutOfRange { .. } => { + vm.new_value_error(err.to_string()) + } + _ => vm.new_system_error(err.to_string()), + } +} + +#[cfg(feature = "codegen")] +fn constant_data_to_py( + constant: rustpython_compiler_core::bytecode::ConstantData, + vm: &VirtualMachine, +) -> PyObjectRef { + use rustpython_compiler_core::bytecode::ConstantData; + match constant { + ConstantData::None => vm.ctx.none(), + ConstantData::Boolean { value } => vm.ctx.new_bool(value).into(), + ConstantData::Integer { value } => vm.ctx.new_int(value).into(), + ConstantData::Float { value } => vm.ctx.new_float(value).into(), + ConstantData::Complex { value } => vm.ctx.new_complex(value).into(), + ConstantData::Str { value } => vm.ctx.new_str(value.to_string()).into(), + ConstantData::Bytes { value } => vm.ctx.new_bytes(value).into(), + ConstantData::Tuple { elements } => vm + .ctx + .new_tuple( + elements + .into_iter() + .map(|element| constant_data_to_py(element, vm)) + .collect(), + ) + .into(), + ConstantData::Frozenset { elements } => crate::builtins::PyFrozenSet::from_iter( + vm, + elements + .into_iter() + .map(|element| constant_data_to_py(element, vm)), + ) + .map_or_else(|_| vm.ctx.none(), |set| set.to_pyobject(vm)), + ConstantData::Ellipsis => vm.ctx.ellipsis.clone().into(), + ConstantData::Code { code } => vm.ctx.new_code(*code).into(), + ConstantData::Slice { elements } => { + let [start, stop, step] = *elements; + crate::builtins::PySlice { + start: Some(constant_data_to_py(start, vm)), + stop: constant_data_to_py(stop, vm), + step: Some(constant_data_to_py(step, vm)), + } + .to_pyobject(vm) + } + } +} + +#[cfg(feature = "codegen")] +fn py_to_constant_data( + obj: PyObjectRef, + vm: &VirtualMachine, +) -> PyResult { + use rustpython_compiler_core::bytecode::ConstantData; + if vm.is_none(&obj) { + return Ok(ConstantData::None); + } + if obj.class().is(vm.ctx.types.bool_type) { + return Ok(ConstantData::Boolean { + value: obj.is_true(vm)?, + }); + } + if let Ok(int) = obj.clone().downcast::() { + return Ok(ConstantData::Integer { + value: int.as_bigint().clone(), + }); + } + if let Ok(float) = obj.clone().downcast::() { + return Ok(ConstantData::Float { + value: float.to_f64(), + }); + } + if let Ok(complex) = obj.clone().downcast::() { + return Ok(ConstantData::Complex { + value: complex.to_complex(), + }); + } + if obj.class().is(vm.ctx.types.str_type) { + return Ok(ConstantData::Str { + value: obj.try_to_value::(vm)?.into(), + }); + } + if obj.class().is(vm.ctx.types.bytes_type) { + return Ok(ConstantData::Bytes { + value: obj.try_to_value(vm)?, + }); + } + if obj.class().is(vm.ctx.types.ellipsis_type) { + return Ok(ConstantData::Ellipsis); + } + if obj.class().is(vm.ctx.types.tuple_type) { + let tuple = obj.downcast::().unwrap(); + let mut elements = Vec::with_capacity(tuple.len()); + for item in tuple.iter() { + elements.push(py_to_constant_data(item.clone(), vm)?); + } + return Ok(ConstantData::Tuple { elements }); + } + if let Ok(code) = obj.clone().downcast::() { + return Ok(ConstantData::Code { + code: Box::new( + code.code + .map_clone_bag(&rustpython_compiler_core::bytecode::BasicBag), + ), + }); + } + Err(vm.new_type_error(format!( + "cannot convert {} to a code constant", + obj.class().name() + ))) +} + +#[cfg(feature = "codegen")] +fn metadata_to_code_unit( + metadata: &Py, + vm: &VirtualMachine, +) -> PyResult { + let name = dict_str(metadata, "name", vm)?.unwrap_or_else(|| "name".to_owned()); + let qualname = dict_str(metadata, "qualname", vm)?.or_else(|| Some(name.clone())); + let consts = dict_consts(metadata, vm)?; + let names = dict_keys_inorder(metadata, "names", vm)?; + let varnames = dict_keys_inorder(metadata, "varnames", vm)?; + let cellvars = dict_keys_inorder(metadata, "cellvars", vm)?; + let freevars = dict_keys_inorder(metadata, "freevars", vm)?; + let mut unit = rustpython_codegen::ir::CodeUnitMetadata { + name, + qualname, + consts: rustpython_codegen::ir::ConstantPool::from_ordered(consts), + names: names.into_iter().collect(), + varnames: varnames.into_iter().collect(), + cellvars: cellvars.into_iter().collect(), + freevars: freevars.into_iter().collect(), + fast_hidden: Default::default(), + fast_hidden_final: Default::default(), + argcount: dict_nonneg_int(metadata, "argcount", vm)?, + posonlyargcount: dict_nonneg_int(metadata, "posonlyargcount", vm)?, + kwonlyargcount: dict_nonneg_int(metadata, "kwonlyargcount", vm)?, + firstlineno: rustpython_compiler_core::OneIndexed::new( + dict_nonneg_int(metadata, "firstlineno", vm)?.max(1) as usize, + ) + .unwrap_or(rustpython_compiler_core::OneIndexed::MIN), + }; + if let Some(value) = metadata.get_item_opt("fasthidden", vm)? + && let Ok(dict) = value.downcast::() + { + for (hidden_name, flag) in dict { + unit.fast_hidden + .insert(hidden_name.try_to_value(vm)?, flag.is_true(vm)?); + } + } + Ok(unit) +} + +#[cfg(feature = "codegen")] +fn dict_str(metadata: &Py, key: &str, vm: &VirtualMachine) -> PyResult> { + let Some(value) = metadata.get_item_opt(key, vm)? else { + return Ok(None); + }; + if vm.is_none(&value) { + return Ok(None); + } + Ok(Some(value.try_to_value(vm)?)) +} + +#[cfg(feature = "codegen")] +fn dict_nonneg_int(metadata: &Py, key: &str, vm: &VirtualMachine) -> PyResult { + let Some(value) = metadata.get_item_opt(key, vm)? else { + return Ok(0); + }; + let n: i32 = value.try_to_value(vm)?; + Ok(u32::try_from(n).unwrap_or(0)) +} + +#[cfg(feature = "codegen")] +fn dict_keys_inorder( + metadata: &Py, + key: &str, + vm: &VirtualMachine, +) -> PyResult> { + let Some(value) = metadata.get_item_opt(key, vm)? else { + return Ok(Vec::new()); + }; + if vm.is_none(&value) { + return Ok(Vec::new()); + } + if let Ok(dict) = value.clone().downcast::() { + let mut items = Vec::new(); + for (name, index) in dict { + let name: String = name.try_to_value(vm)?; + let index: i32 = index.try_to_value(vm)?; + items.push((index, name)); + } + items.sort_by_key(|(index, _)| *index); + return Ok(items.into_iter().map(|(_, name)| name).collect()); + } + if let Ok(list) = value.downcast::() { + return list + .borrow_vec() + .iter() + .map(|item| item.try_to_value(vm)) + .collect(); + } + Ok(Vec::new()) +} + +#[cfg(feature = "codegen")] +fn dict_consts( + metadata: &Py, + vm: &VirtualMachine, +) -> PyResult> { + let Some(value) = metadata.get_item_opt("consts", vm)? else { + return Ok(Vec::new()); + }; + if let Ok(list) = value.clone().downcast::() { + let mut consts = Vec::new(); + for item in list.borrow_vec().iter() { + consts.push(py_to_constant_data(item.clone(), vm)?); + } + return Ok(consts); + } + if let Ok(dict) = value.downcast::() { + let mut items = Vec::new(); + for (constant, index) in dict { + let index: i32 = index.try_to_value(vm)?; + items.push((index, py_to_constant_data(constant, vm)?)); + } + items.sort_by_key(|(index, _)| *index); + return Ok(items.into_iter().map(|(_, constant)| constant).collect()); + } + Ok(Vec::new()) +} diff --git a/crates/vm/src/stdlib/mod.rs b/crates/vm/src/stdlib/mod.rs index 40ee2eda8..db5dfea1c 100644 --- a/crates/vm/src/stdlib/mod.rs +++ b/crates/vm/src/stdlib/mod.rs @@ -14,6 +14,7 @@ mod _string; mod _symtable; mod _sysconfig; mod _sysconfigdata; +pub(crate) mod _testinternalcapi; mod _types; pub mod _typing; pub mod _warnings; @@ -133,6 +134,7 @@ pub fn builtin_module_defs(ctx: &Context) -> Vec<&'static PyModuleDef> { _sre::module_def(ctx), _stat::module_def(ctx), _string::module_def(ctx), + _testinternalcapi::module_def(ctx), #[cfg(feature = "compiler")] _symtable::module_def(ctx), _sysconfigdata::module_def(ctx), diff --git a/crates/vm/src/stdlib/sys.rs b/crates/vm/src/stdlib/sys.rs index c36ac6fb6..c12d11a68 100644 --- a/crates/vm/src/stdlib/sys.rs +++ b/crates/vm/src/stdlib/sys.rs @@ -401,6 +401,7 @@ pub mod sys { "_suggestions", "_symtable", "_sysconfig", + "_testinternalcapi", "_thread", "_threading_local", "_tkinter", diff --git a/crates/vm/src/types/slot.rs b/crates/vm/src/types/slot.rs index b6aa3ad69..edde96404 100644 --- a/crates/vm/src/types/slot.rs +++ b/crates/vm/src/types/slot.rs @@ -227,6 +227,7 @@ bitflags! { #[derive(Copy, Clone, Debug, PartialEq)] #[non_exhaustive] pub struct PyTypeFlags: u64 { + const INLINE_VALUES = 1 << 2; const MANAGED_WEAKREF = 1 << 3; const MANAGED_DICT = 1 << 4; const SEQUENCE = 1 << 5; @@ -549,6 +550,62 @@ fn getattro_wrapper(zelf: &PyObject, name: &Py, vm: &VirtualMachine) -> P } } +/// hackcheck: reject object.__setattr__/__delattr__ applied to a type +/// whose C-level tp_setattro is not the wrapped function. +pub(crate) fn hackcheck_setattro( + obj: &PyObject, + func: SetattroFunc, + what: &str, + vm: &VirtualMachine, +) -> PyResult<()> { + let Some(_typ) = obj.downcast_ref::() else { + return Ok(()); + }; + let obj_cls = obj.class(); + let obj_setattro = obj_cls.slots.setattro.load(); + + let mut defining = obj_cls.to_owned(); + { + let mro = obj_cls.mro.read(); + for base in mro.iter().rev() { + let base_setattro = base.slots.setattro.load(); + if is_slot_tp_setattro(base_setattro) { + continue; + } else if setattro_eq(base_setattro, obj_setattro) { + defining = base.clone(); + break; + } + } + } + + let mut base = Some(defining); + while let Some(b) = base { + let b_setattro = b.slots.setattro.load(); + if setattro_eq(b_setattro, Some(func)) { + return Ok(()); + } else if !is_slot_tp_setattro(b_setattro) { + return Err(vm.new_type_error(format!( + "can't apply this {what} to {} object", + obj_cls.slot_name() + ))); + } + base = b.base.deref().map(|cls| cls.to_owned()); + } + Ok(()) +} + +fn is_slot_tp_setattro(f: Option) -> bool { + f.is_some_and(|f| fn_addr(f) == fn_addr(setattro_wrapper as SetattroFunc)) +} + +fn setattro_eq(a: Option, b: Option) -> bool { + match (a, b) { + (Some(a), Some(b)) => fn_addr(a) == fn_addr(b), + (None, None) => true, + _ => false, + } +} + fn setattro_wrapper( zelf: &PyObject, name: &Py, @@ -573,8 +630,13 @@ pub(crate) fn richcompare_wrapper( op: PyComparisonOp, vm: &VirtualMachine, ) -> PyResult> { - vm.call_special_method(zelf, op.method_name(&vm.ctx), (other.to_owned(),)) - .map(Either::A) + // slot_tp_richcompare / _PyObject_MaybeCallSpecialOneArg: a missing + // method, or a data descriptor whose __get__ raises AttributeError, + // is NotImplemented rather than an error. + match vm.get_special_method(zelf, op.method_name(&vm.ctx))? { + Some(meth) => meth.invoke((other.to_owned(),), vm).map(Either::A), + None => Ok(Either::B(PyComparisonValue::NotImplemented)), + } } fn iter_wrapper(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult { @@ -654,7 +716,7 @@ fn init_wrapper(obj: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResu } pub(crate) fn new_wrapper(cls: PyTypeRef, mut args: FuncArgs, vm: &VirtualMachine) -> PyResult { - let new = cls.get_attr(identifier!(vm, __new__)).unwrap(); + let new = cls.as_object().get_attr(identifier!(vm, __new__), vm)?; args.prepend_arg(cls.into()); new.call(args, vm) } diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index 53c743931..3c7e806aa 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -264,6 +264,7 @@ declare_const_name! { items, keys, modules, + mro, n_fields, n_sequence_fields, n_unnamed_fields, diff --git a/crates/vm/src/vm/crossinterp.rs b/crates/vm/src/vm/crossinterp.rs index 31a4e3639..b7292d882 100644 --- a/crates/vm/src/vm/crossinterp.rs +++ b/crates/vm/src/vm/crossinterp.rs @@ -566,7 +566,7 @@ fn verify_stateless( } /// The `co_names` entries `identify_unbound_names` reaches through `LOAD_GLOBAL`. -fn global_names(code: &PyCode) -> impl Iterator + '_ { +pub(crate) fn global_names(code: &PyCode) -> impl Iterator + '_ { walk_instructions(code).filter_map(|(_, instr, arg)| match instr { Instruction::LoadGlobal { namei } => Some(code.names[(namei.get(arg) >> 1) as usize]), _ => None, @@ -575,7 +575,9 @@ fn global_names(code: &PyCode) -> impl Iterator + /// The instruction stream with its inline caches skipped and its specialized /// and instrumented opcodes mapped back, yielding `(offset, instruction, arg)`. -fn walk_instructions(code: &PyCode) -> impl Iterator + '_ { +pub(crate) fn walk_instructions( + code: &PyCode, +) -> impl Iterator + '_ { let units = &code.instructions; let mut arg_state = OpArgState::default(); let mut offset = 0; @@ -645,7 +647,7 @@ fn is_pure_function(code: &PyCode) -> bool { /// `_PyCode_ReturnsOnlyNone`. Here "value" means a non-None value, since a bare /// return is identical to returning None explicitly, as is a missing return /// statement at the end of the function. -fn code_returns_only_none(code: &PyCode) -> bool { +pub(crate) fn code_returns_only_none(code: &PyCode) -> bool { if !is_pure_function(code) { return false; } diff --git a/crates/vm/src/vm/method.rs b/crates/vm/src/vm/method.rs index 2beeb95ce..e53289c6c 100644 --- a/crates/vm/src/vm/method.rs +++ b/crates/vm/src/vm/method.rs @@ -91,6 +91,21 @@ impl PyMethod { obj: &PyObject, name: &'static PyStrInterned, vm: &VirtualMachine, + ) -> PyResult> { + Self::get_special_ex::(obj, name, vm, false) + } + + /// lookup_method_ex. + /// + /// `raise_attribute_error` is CPython's same-named argument: when false + /// (`lookup_maybe_method`), AttributeError from `tp_descr_get` is cleared + /// and the lookup reports a miss. When true (`lookup_method`), that error + /// is kept. + pub(crate) fn get_special_ex( + obj: &PyObject, + name: &'static PyStrInterned, + vm: &VirtualMachine, + raise_attribute_error: bool, ) -> PyResult> { let obj_cls = obj.class(); let attr = if DIRECT { @@ -116,9 +131,18 @@ impl PyMethod { } } else { let obj_cls = obj_cls.to_owned().into(); - let attr = vm - .call_get_descriptor_specific(&func, Some(obj.to_owned()), Some(obj_cls)) - .unwrap_or(Ok(func))?; + let attr = + match vm.call_get_descriptor_specific(&func, Some(obj.to_owned()), Some(obj_cls)) { + Some(Ok(attr)) => attr, + Some(Err(e)) + if !raise_attribute_error + && e.fast_isinstance(vm.ctx.exceptions.attribute_error) => + { + return Ok(None); + } + Some(Err(e)) => return Err(e), + None => func, + }; Self::Attribute(attr) }; Ok(Some(meth)) diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index ac6465e28..37f2f4e1d 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -2104,7 +2104,7 @@ impl VirtualMachine { #[cfg_attr(any(miri, target_env = "musl"), allow(dead_code))] // 2× CPython's _PY_STACK_MARGIN_BYTES to account for both heavy and // light frame native stack usage per recursion step. - const STACK_MARGIN_BYTES: usize = + pub(crate) const STACK_MARGIN_BYTES: usize = (if cfg!(debug_assertions) { 16384 } else { 4096 }) * core::mem::size_of::(); /// How deep native recursion may go where the stack cannot be measured diff --git a/crates/vm/src/vm/vm_object.rs b/crates/vm/src/vm/vm_object.rs index aa68f7f4d..9ab26ddff 100644 --- a/crates/vm/src/vm/vm_object.rs +++ b/crates/vm/src/vm/vm_object.rs @@ -176,7 +176,8 @@ impl VirtualMachine { method: &'static PyStrInterned, args: impl IntoFuncArgs, ) -> PyResult { - self.get_special_method(obj, method)? + // lookup_method: AttributeError from a data descriptor is kept. + PyMethod::get_special_ex::(obj, method, self, true)? .ok_or_else(|| self.new_attribute_error(method.as_str().to_owned()))? .invoke(args, self) } diff --git a/tools/opcode_metadata/generate_rs_opcode_metadata.py b/tools/opcode_metadata/generate_rs_opcode_metadata.py index 97482f337..b0a7e3f37 100644 --- a/tools/opcode_metadata/generate_rs_opcode_metadata.py +++ b/tools/opcode_metadata/generate_rs_opcode_metadata.py @@ -46,7 +46,22 @@ class OpcodeGen: @property def fn_has_arg(self) -> str: - return self.gen_fn_has_attr("has_arg", "oparg", "HAS_ARG_FLAG") + arms = "|".join( + f"Self::{opcode.rust_name}" + for opcode in self + if opcode.properties.oparg or opcode.have_argument + ) + if arms: + inner = f"matches!(self, {arms})" + else: + inner = "false" + return f""" + /// Does this opcode have 'HAS_ARG_FLAG' set. + #[must_use] + pub const fn has_arg(self) -> bool {{ + {inner} + }} + """ @property def fn_has_const(self) -> str: diff --git a/tools/opcode_metadata/opcodes.py b/tools/opcode_metadata/opcodes.py index c1b198017..9178921b2 100644 --- a/tools/opcode_metadata/opcodes.py +++ b/tools/opcode_metadata/opcodes.py @@ -82,7 +82,7 @@ def iter_opcodes(text: str, override_confs: OverrideConfs) -> Iterable[Opcode]: kwargs = {} if instr := analysis.instructions.get(cpython_name): - kwargs["properties"] = instr.properties + kwargs["properties"] = properties_with_pseudo_flags(instr) kwargs["family"] = getattr(instr, "family", None) kwargs["cache_entry"] = getattr(instr, "size", -1) @@ -100,6 +100,33 @@ def iter_opcodes(text: str, override_confs: OverrideConfs) -> Iterable[Opcode]: yield dataclasses.replace(opcode, override=override, **kwargs) +# opcode_metadata_generator.py generate_metadata_table: cflags(pseudo.properties) +# plus each explicit flag from the pseudo() definition. +_PSEUDO_FLAG_ATTRS = { + "HAS_ARG": "oparg", + "HAS_CONST": "uses_co_consts", + "HAS_NAME": "uses_co_names", + "HAS_JUMP": "jumps", + "HAS_FREE": "has_free", + "HAS_LOCAL": "uses_locals", + "HAS_EVAL_BREAK": "eval_breaker", + "HAS_DEOPT": "deopts", + "HAS_EXIT": "side_exit", + "HAS_PURE": "pure", +} + + +def properties_with_pseudo_flags(instr): + props = instr.properties + flags = getattr(instr, "flags", None) + if not flags: + return props + updates = {attr: True for flag in flags if (attr := _PSEUDO_FLAG_ATTRS.get(flag))} + if not updates: + return props + return dataclasses.replace(props, **updates) + + @dataclasses.dataclass(frozen=True, slots=True) class Opcode: rust_name: str