Honor inline values, custom mro, and type lookup (#8729)

* Add inline values flags and special-method lookup

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

Assisted-by: Grok:grok-4.6

* Call custom mro and descr_get for type.__new__

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

Assisted-by: Grok:grok-4.6

* Expose _testinternalcapi recursion and rare events

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

Assisted-by: Grok:grok-4.6

* Add remaining _testinternalcapi code and locale APIs

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

Assisted-by: Grok:grok-4.6

* Fix lint for _testinternalcapi identifiers

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

Assisted-by: Grok:grok-4.6

* Expand _testinternalcapi with remaining test APIs

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

Assisted-by: Grok:grok-4.6

* Expose compiler IR APIs in _testinternalcapi

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

Assisted-by: Grok:grok-4.6

* Match optimize_cfg consts and addop locations

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

Assisted-by: Grok:grok-4.6

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

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

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

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

Assisted-by: Grok:grok-4.6

* Skip FOR_ITER RAISE for sequence iterators

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

Assisted-by: Grok:grok-4.6

* Fix generator FOR_ITER events and review issues

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

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

Assisted-by: Grok:grok-4.6
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-09-19 00:40:20 +09:00
1 parent 982cbd4d25
commit 300d8bdc02
36 files changed
+2970 -194

No files matched your search

+12
View File
@@ -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
+1 -11
View File
@@ -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()
-13
View File
@@ -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
+2
View File
@@ -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):
-1
View File
@@ -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:
+1 -2
View File
@@ -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):
-1
View File
@@ -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
+107 -2
View File
@@ -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 <feature>`.
@@ -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<ConstantData>,
}
/// compile.c _PyCompile_CodeGen
pub fn compile_codegen(
ast: ruff_python_ast::Mod,
source_file: SourceFile,
mode: Mode,
opts: CompileOpts,
) -> CompileResult<CodegenOutput> {
compile_codegen_with_syntax_warning_handler(ast, source_file, mode, opts, None)
}
fn compile_codegen_with_syntax_warning_handler<'a>(
mut ast: ruff_python_ast::Mod,
source_file: SourceFile,
mode: Mode,
mut opts: CompileOpts,
mut syntax_warning_handler: Option<&'a mut SyntaxWarningHandler<'a>>,
) -> CompileResult<CodegenOutput> {
opts.future_features |= checked_future_features(&ast, &source_file)?;
let future_annotations = opts
.future_features
.contains(bytecode::CodeFlags::FUTURE_ANNOTATIONS);
if let Some(handler) = syntax_warning_handler.as_deref_mut() {
preprocess::warn_control_flow_in_finally(&ast, |range, message| {
warn_ast_preprocess_syntax(&source_file, handler, range, message)
})?;
}
if matches!(mode, Mode::Single)
&& let ruff_python_ast::Mod::Module(module) = &mut ast
{
preprocess::preprocess_statements(
&mut module.body,
opts.optimize,
future_annotations,
false,
);
} else {
preprocess::preprocess_mod(&mut ast, opts.optimize, future_annotations, false);
}
let mut compiler = Compiler::new_with_syntax_warning_handler(
opts,
source_file.clone(),
"<module>",
syntax_warning_handler,
);
compiler.save_nested_seqs = true;
match ast {
ruff_python_ast::Mod::Module(module) => match mode {
Mode::Exec | Mode::Eval => {
let symbol_table = scan_module_symbols(&module, &source_file, &compiler.opts)?;
compiler.compile_program(&module, symbol_table)?;
}
Mode::Single => {
let symbol_table = scan_module_symbols(&module, &source_file, &compiler.opts)?;
compiler.compile_program_single(&module.body, symbol_table)?;
}
Mode::BlockExpr => {
let symbol_table = scan_module_symbols(&module, &source_file, &compiler.opts)?;
compiler.compile_block_expr(&module.body, symbol_table)?;
}
},
ruff_python_ast::Mod::Expression(expr) => {
let symbol_table = scan_expr_symbols(&expr, &source_file, &compiler.opts)?;
compiler.compile_eval(&expr, symbol_table)?;
}
}
let mut unit = compiler
.code_stack
.pop()
.expect("codegen leaves the top-level unit on the stack");
unit.instr_sequence.apply_label_map();
Ok(CodegenOutput {
seq: unit.instr_sequence,
argcount: unit.metadata.argcount,
posonlyargcount: unit.metadata.posonlyargcount,
kwonlyargcount: unit.metadata.kwonlyargcount,
consts: unit.metadata.consts.into_vec(),
})
}
/// Compile a standard Python program to bytecode
pub fn compile_program(
ast: &ast::ModModule,
@@ -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
}
+7
View File
@@ -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})"
)
}
}
}
}
+279 -3
View File
@@ -204,6 +204,16 @@ impl ConstantPool {
self.insert_full(constant).1
}
#[must_use]
pub fn from_ordered(constants: Vec<ConstantData>) -> Self {
Self { constants }
}
#[must_use]
pub fn into_vec(self) -> Vec<ConstantData> {
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<InstructionSequenceEntry>,
/// CPython `instr_sequence.s_allocated`, the allocated size of `s_instrs`.
@@ -641,12 +653,116 @@ pub(crate) struct InstructionSequence {
label_map: Option<Vec<i32>>,
label_map_allocation: usize,
annotations_code: Option<Box<Self>>,
nested: Vec<Self>,
}
/// One instruction as returned by InstructionSequence.get_instructions.
#[derive(Clone, Copy, Debug)]
pub struct PythonInstruction {
pub opcode: i32,
pub oparg: Option<i32>,
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<Self> {
&mut self.nested
}
#[must_use]
pub fn python_instructions(&self) -> Vec<PythonInstruction> {
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<ConstantData>,
nlocals: usize,
) -> crate::InternalResult<(InstructionSequence, Vec<ConstantData>)> {
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<CodeObject> {
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,
@@ -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());
+3
View File
@@ -929,6 +929,9 @@ impl super::PseudoOpcode {
| Self::JumpIfTrue
| Self::JumpNoInterrupt
| Self::LoadClosure
| Self::SetupCleanup
| Self::SetupFinally
| Self::SetupWith
| Self::StoreFastMaybeNull
)
}
+2 -3
View File
@@ -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
};
+8 -1
View File
@@ -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<PyStr>, 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<PyStr>,) = args.bind(vm)?;
crate::types::hackcheck_setattro(&obj, *func, "__delattr__", vm)?;
func(&obj, &name, PySetterValue::Delete, vm)?;
Ok(vm.ctx.none())
}
+19 -2
View File
@@ -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<PyDict> {
Ok(attrs)
}
pub(crate) fn get_item_known_hash(
&self,
key: &PyObject,
hash: crate::common::hash::PyHash,
vm: &VirtualMachine,
) -> PyResult<Option<PyObjectRef>> {
self.entries.get_known_hash(vm, key, hash)
}
pub fn get_item_opt<K: DictKey + ?Sized>(
&self,
key: &K,
@@ -927,7 +940,11 @@ impl Py<PyDict> {
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)
}
+3
View File
@@ -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},
+4
View File
@@ -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__"))
}
+6 -9
View File
@@ -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::<PyType>()
&& !cls.is(&ty)
&& !cls.is(obj.class())
&& cls.fast_issubclass(&ty)
{
return Ok(cls);
+270 -98
View File
@@ -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<PyRef<
}
fn is_subtype_with_mro(a_mro: &[PyTypeRef], a: &Py<PyType>, b: &Py<PyType>) -> 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<R>(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<bool> = 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<Self>,
ctx: &Context,
defer_mro: bool,
) -> Result<PyRef<Self>, 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<PyObjectRef> {
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<PyType> {
/// 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<PyObject>) -> 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<F, R>(&self, f: F) -> Vec<R>
@@ -1682,8 +1736,6 @@ impl PyType {
}
let bases = bases_tuple.try_into_typed::<Self>(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<PyType>,
undo: &mut Vec<(PyTypeRef, Vec<PyTypeRef>)>,
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<PyType> = subclass.downcast_ref().unwrap();
update_mro_recursively(subclass, undo, vm)?;
) -> PyResult<i32> {
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<PyTypeRef> = cls
.subclasses
.read()
.iter()
.filter_map(|subclass| subclass.upgrade())
.filter_map(|subclass| subclass.downcast::<PyType>().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<PyStrRef> = 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<PyStrRef> = 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<PyType> {
#[pygetset]
fn __mro__(&self) -> PyTuple {
let elements: Vec<PyObjectRef> = 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<PyObjectRef> = 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<PyType> {
}
#[pymethod]
fn mro(&self) -> Vec<PyObjectRef> {
self.mro_map_collect(|cls| cls.to_owned().into())
fn mro(&self, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
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<PyType>, b: &Py<PyType>, 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<PyType>, vm: &VirtualMachine) -> PyResult<Vec<PyTypeRef>> {
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<PyType>, mro: &[PyObjectRef], vm: &VirtualMachine) -> PyResult<()> {
let solid = solid_base(typ, vm).to_owned();
for obj in mro {
let Some(base) = obj.downcast_ref::<PyType>() 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<PyType>, vm: &VirtualMachine) -> PyResult<Vec<PyTypeRef>> {
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<PyObjectRef> = 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::<PyType>().map_err(|obj| {
vm.new_type_error(format!(
"mro() returned a non-class ('{}')",
obj.class().name()
))
})
})
.collect()
}
fn mro_internal(typ: &Py<PyType>, vm: &VirtualMachine) -> PyResult<i32> {
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<PyType>, t2: &Py<PyType>) -> bool {
t1.__basicsize__() != t2.__basicsize__() || t1.slots.itemsize != t2.slots.itemsize
@@ -3492,8 +3665,7 @@ fn same_slots_added(a: &Py<PyType>, b: &Py<PyType>) -> bool {
.zip(y.iter())
.all(|(p, q)| p.as_wtf8() == q.as_wtf8())
}
(None, None) => true,
_ => false,
_ => true,
}
}
+15
View File
@@ -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<T: Clone> Dict<T> {
self._get_inner(vm, key, hash)
}
/// `_PyDict_GetItem_KnownHash`: lookup using a caller-supplied hash.
pub(crate) fn get_known_hash<K: DictKey + ?Sized>(
&self,
vm: &VirtualMachine,
key: &K,
hash: HashValue,
) -> PyResult<Option<T>> {
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
+46 -7
View File
@@ -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<PyObjectRef>,
vm: &VirtualMachine,
) -> PyResult<()> {
let iter = self.top_value();
if iter.downcast_ref_if_exact::<PyListIterator>(vm).is_some()
|| iter.downcast_ref_if_exact::<PyRangeIterator>(vm).is_some()
|| iter.downcast_ref_if_exact::<PyTupleIterator>(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)
}
+53 -13
View File
@@ -372,10 +372,15 @@ pub(super) struct ObjExt {
}
impl ObjExt {
fn new(dict: Option<PyDictRef>, member_count: usize, has_dict: bool) -> Self {
fn new(
dict: Option<PyDictRef>,
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<PyWeak> {
}
}
/// 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<Option<PyDictRef>>,
inline_values_valid: PyAtomic<bool>,
}
impl From<PyDictRef> for InstanceDict {
@@ -1093,16 +1103,35 @@ impl From<PyDictRef> 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<PyDictRef>, 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<PyDictRef>) -> 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<T: PyPayload + core::fmt::Debug> PyInner<T> {
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<PyDictRef> {
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()
}
+18 -14
View File
@@ -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<PyStr>, vm: &VirtualMachine) -> Option<PyObjectRef> {
/// _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<PyStr>,
vm: &VirtualMachine,
) -> PyResult<Option<PyObjectRef>> {
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))
}
}
}
+39
View File
@@ -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<RustModFromObject> {
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,
+9 -1
View File
@@ -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<InterpreterConfig> {
parse_config(Some(obj), vm)
}
/// `config_from_object`.
fn parse_config(obj: Option<&PyObject>, vm: &VirtualMachine) -> PyResult<InterpreterConfig> {
let Some(obj) = obj.filter(|o| !vm.is_none(o)) else {
File diff suppressed because it is too large. Load diff
+2
View File
@@ -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),
+1
View File
@@ -401,6 +401,7 @@ pub mod sys {
"_suggestions",
"_symtable",
"_sysconfig",
"_testinternalcapi",
"_thread",
"_threading_local",
"_tkinter",
+65 -3
View File
@@ -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<PyStr>, 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::<PyType>() 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<SetattroFunc>) -> bool {
f.is_some_and(|f| fn_addr(f) == fn_addr(setattro_wrapper as SetattroFunc))
}
fn setattro_eq(a: Option<SetattroFunc>, b: Option<SetattroFunc>) -> 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<PyStr>,
@@ -573,8 +630,13 @@ pub(crate) fn richcompare_wrapper(
op: PyComparisonOp,
vm: &VirtualMachine,
) -> PyResult<Either<PyObjectRef, PyComparisonValue>> {
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)
}
+1
View File
@@ -264,6 +264,7 @@ declare_const_name! {
items,
keys,
modules,
mro,
n_fields,
n_sequence_fields,
n_unnamed_fields,
+5 -3
View File
@@ -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<Item = &'static PyStrInterned> + '_ {
pub(crate) fn global_names(code: &PyCode) -> impl Iterator<Item = &'static PyStrInterned> + '_ {
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<Item = &'static PyStrInterned> +
/// 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<Item = (usize, Instruction, OpArg)> + '_ {
pub(crate) fn walk_instructions(
code: &PyCode,
) -> impl Iterator<Item = (usize, Instruction, OpArg)> + '_ {
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;
}
+27 -3
View File
@@ -91,6 +91,21 @@ impl PyMethod {
obj: &PyObject,
name: &'static PyStrInterned,
vm: &VirtualMachine,
) -> PyResult<Option<Self>> {
Self::get_special_ex::<DIRECT>(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<const DIRECT: bool>(
obj: &PyObject,
name: &'static PyStrInterned,
vm: &VirtualMachine,
raise_attribute_error: bool,
) -> PyResult<Option<Self>> {
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))
+1 -1
View File
@@ -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::<usize>();
/// How deep native recursion may go where the stack cannot be measured
+2 -1
View File
@@ -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::<false>(obj, method, self, true)?
.ok_or_else(|| self.new_attribute_error(method.as_str().to_owned()))?
.invoke(args, self)
}
@@ -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:
+28 -1
View File
@@ -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