Skip to content

Commit d40cd9f

Browse files
committed
Match bare super and static type itemsize
`super.__new__(super)` leaves `__thisclass__`, `__self__` and `__self_class__` empty, and a super with no type takes the zero-argument path when bound. Static types carry their `tp_itemsize` (`type`, `memoryview`, `code`, `frame`, generators, `_sre` objects, `_ctypes` thunks), inherit it from their direct base when they leave it at 0, and a variable-size base rejects any nonempty `__slots__` and a weakref slot. `_thread._ExceptHookArgs` is a struct sequence. Assisted-by: Grok:4.7 Assisted-by: Claude:claude-opus-5-5
1 parent 5c5bc80 commit d40cd9f

13 files changed

Lines changed: 233 additions & 152 deletions

File tree

‎crates/vm/src/builtins/asyncgenerator.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ impl PyPayload for PyAsyncGen {
6060
}
6161

6262
#[pyclass(
63+
itemsize = core::mem::size_of::<crate::PyObjectRef>(),
6364
flags(DISALLOW_INSTANTIATION, HAS_WEAKREF),
6465
with(PyRef, Representable, Destructor)
6566
)]

‎crates/vm/src/builtins/code.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,7 @@ impl Constructor for PyCode {
10411041
}
10421042

10431043
#[pyclass(
1044+
itemsize = core::mem::size_of::<u16>(),
10441045
with(Representable, Constructor, Comparable, Hashable),
10451046
flags(HAS_WEAKREF)
10461047
)]

‎crates/vm/src/builtins/coroutine.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ impl PyPayload for PyCoroutine {
4242
}
4343

4444
#[pyclass(
45+
itemsize = core::mem::size_of::<crate::PyObjectRef>(),
4546
flags(DISALLOW_INSTANTIATION, HAS_WEAKREF),
4647
with(Py, Representable, Destructor)
4748
)]

‎crates/vm/src/builtins/descriptor.rs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,18 @@ pub enum MemberAccess {
590590
TupleItem,
591591
}
592592

593+
/// C layout of `PyMemberDef`: name pointer, int type, `Py_ssize_t` offset,
594+
/// int flags, doc pointer. `size_of` of this is `type`'s `tp_itemsize`.
595+
#[repr(C)]
596+
#[allow(dead_code)]
597+
pub struct PyMemberDefLayout {
598+
_name: *const core::ffi::c_char,
599+
_type: core::ffi::c_int,
600+
_offset: isize,
601+
_flags: core::ffi::c_int,
602+
_doc: *const core::ffi::c_char,
603+
}
604+
593605
/// Same fields as `PyMemberDef`: name, type, offset, flags, doc.
594606
pub struct PyMemberDef {
595607
pub name: String,

‎crates/vm/src/builtins/frame.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,11 @@ impl FrameObject {
465465
}
466466
}
467467

468-
#[pyclass(flags(DISALLOW_INSTANTIATION), with(Py, Representable))]
468+
#[pyclass(
469+
itemsize = core::mem::size_of::<crate::PyObjectRef>(),
470+
flags(DISALLOW_INSTANTIATION),
471+
with(Py, Representable)
472+
)]
469473
impl FrameObject {
470474
#[pygetset]
471475
pub fn f_globals(&self) -> PyDictRef {

‎crates/vm/src/builtins/generator.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ impl PyPayload for PyGenerator {
3939
}
4040

4141
#[pyclass(
42+
itemsize = core::mem::size_of::<crate::PyObjectRef>(),
4243
flags(DISALLOW_INSTANTIATION, HAS_WEAKREF),
4344
with(Py, IterNext, Iterable, Representable, Destructor)
4445
)]

‎crates/vm/src/builtins/memory.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,7 @@ impl Py<PyMemoryView> {
675675
}
676676

677677
#[pyclass(
678+
itemsize = core::mem::size_of::<isize>(),
678679
with(
679680
Py,
680681
Hashable,

‎crates/vm/src/builtins/super.rs‎

Lines changed: 115 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use crate::{
1818
#[derive(Debug)]
1919
pub struct PySuper {
2020
#[pymember(name = "__thisclass__")]
21-
typ: PyAtomicRef<PyType>,
21+
typ: PyAtomicRef<Option<PyType>>,
2222
#[pymember(name = "__self__")]
2323
obj: PyAtomicRef<Option<PyObject>>,
2424
#[pymember(name = "__self_class__")]
@@ -38,22 +38,23 @@ fn bind_super(
3838
}
3939

4040
impl PySuper {
41-
fn new_bound(typ: PyTypeRef, obj: Option<PyObjectRef>, obj_type: Option<PyTypeRef>) -> Self {
41+
fn empty() -> Self {
4242
Self {
43-
typ: PyAtomicRef::from(typ),
44-
obj: PyAtomicRef::from(obj),
45-
obj_type: PyAtomicRef::from(obj_type),
43+
typ: PyAtomicRef::from(None),
44+
obj: PyAtomicRef::from(None),
45+
obj_type: PyAtomicRef::from(None),
4646
}
4747
}
4848

49-
fn store_bound(&self, typ: PyTypeRef, obj: Option<PyObjectRef>, obj_type: Option<PyTypeRef>) {
49+
fn store_bound(
50+
&self,
51+
typ: Option<PyTypeRef>,
52+
obj: Option<PyObjectRef>,
53+
obj_type: Option<PyTypeRef>,
54+
) {
55+
drop(self.typ.store(typ));
5056
drop(self.obj.store(obj));
5157
drop(self.obj_type.store(obj_type));
52-
drop(self.typ.store(typ));
53-
}
54-
55-
fn thisclass(&self) -> PyTypeRef {
56-
self.typ.load_owned()
5758
}
5859
}
5960

@@ -67,13 +68,9 @@ impl PyPayload for PySuper {
6768
impl Constructor for PySuper {
6869
type Args = FuncArgs;
6970

70-
fn py_new(_cls: &Py<PyType>, _args: Self::Args, vm: &VirtualMachine) -> PyResult<Self> {
71-
let (typ, obj, obj_type) = bind_super(
72-
vm.ctx.types.object_type.to_owned(), // is this correct?
73-
vm.ctx.none(),
74-
vm,
75-
)?;
76-
Ok(Self::new_bound(typ, obj, obj_type))
71+
fn py_new(_cls: &Py<PyType>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<Self> {
72+
// `tp_new` leaves every field null. `tp_init` fills them in.
73+
Ok(Self::empty())
7774
}
7875
}
7976

@@ -98,77 +95,93 @@ impl Initializer for PySuper {
9895
Self::Args { py_type, object }: Self::Args,
9996
vm: &VirtualMachine,
10097
) -> PyResult<()> {
101-
// Get the type:
102-
let (typ, obj) = if let OptionalArg::Present(ty) = py_type {
103-
(ty, object.unwrap_or_none(vm))
104-
} else {
105-
// Access the InterpreterFrame directly — no need to materialize
106-
// a FrameObject just to read code/locals.
107-
let iframe_ptr = crate::vm::thread::get_current_frame();
108-
if iframe_ptr.is_null() {
109-
return Err(vm.new_runtime_error("super(): no current frame"));
110-
}
111-
let iframe = unsafe { &*iframe_ptr };
112-
let code = iframe.code();
113-
114-
if code.arg_count == 0 {
115-
return Err(vm.new_runtime_error("super(): no arguments"));
116-
}
98+
let typ = match py_type {
99+
OptionalArg::Present(ty) => Some(ty),
100+
OptionalArg::Missing => None,
101+
};
102+
let obj = match object {
103+
OptionalArg::Present(obj) => Some(obj),
104+
OptionalArg::Missing => None,
105+
};
106+
super_init_impl(zelf, typ, obj, vm)
107+
}
108+
}
117109

118-
// SAFETY: InterpreterFrame is current and not concurrently mutated.
119-
use rustpython_compiler_core::bytecode::CO_FAST_CELL;
120-
let fastlocals = iframe.localsplus.fastlocals();
121-
let obj = fastlocals[0]
122-
.clone()
123-
.and_then(|val| {
124-
// If slot 0 is a merged cell (LOCAL|CELL), extract value from cell
125-
if code
126-
.localspluskinds
127-
.first()
128-
.is_some_and(|&k| k & CO_FAST_CELL != 0)
129-
{
130-
val.downcast_ref::<PyCell>().and_then(|c| c.get())
131-
} else {
132-
Some(val)
133-
}
134-
})
135-
.ok_or_else(|| vm.new_runtime_error("super(): arg[0] deleted"))?;
136-
137-
let mut typ = None;
138-
// Search for __class__ in freevars using localspluskinds
139-
let nlocalsplus = code.localspluskinds.len();
140-
let nfrees = code.freevars.len();
141-
let free_start = nlocalsplus - nfrees;
142-
for (i, var) in code.freevars.iter().enumerate() {
143-
if var.as_bytes() == b"__class__" {
144-
let class = fastlocals[free_start + i]
145-
.as_ref()
146-
.and_then(|v| v.downcast_ref::<PyCell>())
147-
.and_then(|c| c.get())
148-
.ok_or_else(|| vm.new_runtime_error("super(): empty __class__ cell"))?;
149-
typ = Some(class.downcast().map_err(|o| {
150-
vm.new_type_error(format!(
151-
"super(): __class__ is not a type ({})",
152-
o.class().name()
153-
))
154-
})?);
155-
break;
156-
}
157-
}
158-
let typ = typ.ok_or_else(|| {
159-
vm.new_type_error(
160-
"super must be called with 1 argument or from inside class method",
161-
)
162-
})?;
110+
/// `super_init_impl`. A null type takes the zero-argument path.
111+
fn super_init_impl(
112+
zelf: &Py<PySuper>,
113+
typ: Option<PyTypeRef>,
114+
obj: Option<PyObjectRef>,
115+
vm: &VirtualMachine,
116+
) -> PyResult<()> {
117+
let (typ, obj) = match typ {
118+
Some(typ) => (typ, obj.unwrap_or_else(|| vm.ctx.none())),
119+
None => super_init_without_args(vm)?,
120+
};
121+
let (typ, obj, obj_type) = bind_super(typ, obj, vm)?;
122+
zelf.store_bound(Some(typ), obj, obj_type);
123+
Ok(())
124+
}
163125

164-
(typ, obj)
165-
};
126+
fn super_init_without_args(vm: &VirtualMachine) -> PyResult<(PyTypeRef, PyObjectRef)> {
127+
// Access the InterpreterFrame directly — no need to materialize
128+
// a FrameObject just to read code/locals.
129+
let iframe_ptr = crate::vm::thread::get_current_frame();
130+
if iframe_ptr.is_null() {
131+
return Err(vm.new_runtime_error("super(): no current frame"));
132+
}
133+
let iframe = unsafe { &*iframe_ptr };
134+
let code = iframe.code();
166135

167-
let (typ, obj, obj_type) = bind_super(typ, obj, vm)?;
168-
zelf.store_bound(typ, obj, obj_type);
136+
if code.arg_count == 0 {
137+
return Err(vm.new_runtime_error("super(): no arguments"));
138+
}
169139

170-
Ok(())
140+
// SAFETY: InterpreterFrame is current and not concurrently mutated.
141+
use rustpython_compiler_core::bytecode::CO_FAST_CELL;
142+
let fastlocals = iframe.localsplus.fastlocals();
143+
let obj = fastlocals[0]
144+
.clone()
145+
.and_then(|val| {
146+
// If slot 0 is a merged cell (LOCAL|CELL), extract value from cell
147+
if code
148+
.localspluskinds
149+
.first()
150+
.is_some_and(|&k| k & CO_FAST_CELL != 0)
151+
{
152+
val.downcast_ref::<PyCell>().and_then(|c| c.get())
153+
} else {
154+
Some(val)
155+
}
156+
})
157+
.ok_or_else(|| vm.new_runtime_error("super(): arg[0] deleted"))?;
158+
159+
let mut typ = None;
160+
// Search for __class__ in freevars using localspluskinds
161+
let nlocalsplus = code.localspluskinds.len();
162+
let nfrees = code.freevars.len();
163+
let free_start = nlocalsplus - nfrees;
164+
for (i, var) in code.freevars.iter().enumerate() {
165+
if var.as_bytes() == b"__class__" {
166+
let class = fastlocals[free_start + i]
167+
.as_ref()
168+
.and_then(|v| v.downcast_ref::<PyCell>())
169+
.and_then(|c| c.get())
170+
.ok_or_else(|| vm.new_runtime_error("super(): empty __class__ cell"))?;
171+
typ = Some(class.downcast().map_err(|o| {
172+
vm.new_type_error(format!(
173+
"super(): __class__ is not a type ({})",
174+
o.class().name()
175+
))
176+
})?);
177+
break;
178+
}
171179
}
180+
let typ = typ.ok_or_else(|| {
181+
vm.new_type_error("super must be called with 1 argument or from inside class method")
182+
})?;
183+
184+
Ok((typ, obj))
172185
}
173186

174187
#[pyclass(
@@ -200,7 +213,9 @@ impl GetAttr for PySuper {
200213
// Both locks are dropped before any arbitrary Python code
201214
// (the descriptor call below) runs, so they can't be held
202215
// across a call that might re-enter and want them again.
203-
let su_type = zelf.thisclass();
216+
let Some(su_type) = zelf.typ.load_owned() else {
217+
return skip(zelf, name);
218+
};
204219
let descr = {
205220
let mro = start_type.mro.read();
206221
mro.iter()
@@ -239,20 +254,30 @@ impl GetDescriptor for PySuper {
239254
return Ok(zelf_obj.to_owned());
240255
}
241256
let zelf_class = zelf.as_object().class();
242-
let typ = zelf.thisclass();
257+
let typ = zelf.typ.load_owned();
243258
if zelf_class.is(vm.ctx.types.super_type) {
244-
let (typ, obj, obj_type) = bind_super(typ, obj.to_owned(), vm)?;
245-
Ok(Self::new_bound(typ, obj, obj_type).into_ref(&vm.ctx).into())
259+
let newobj = Self::empty().into_ref(&vm.ctx);
260+
// A null type takes the zero-argument path inside super_init_impl.
261+
super_init_impl(&newobj, typ, Some(obj.to_owned()), vm)?;
262+
Ok(newobj.into())
246263
} else {
247-
PyType::call(zelf.class(), (typ, obj.to_owned()).into_args(vm), vm)
264+
// Call stops at the first null argument.
265+
let args = match typ {
266+
Some(typ) => (typ, obj.to_owned()).into_args(vm),
267+
None => FuncArgs::default(),
268+
};
269+
PyType::call(zelf.class(), args, vm)
248270
}
249271
}
250272
}
251273

252274
impl Representable for PySuper {
253275
#[inline]
254276
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
255-
let type_name = zelf.thisclass().name().to_owned();
277+
let type_name = match zelf.typ.load_owned() {
278+
Some(ty) => ty.name().to_owned(),
279+
None => "NULL".to_owned(),
280+
};
256281
let repr = match zelf.obj_type.load_owned() {
257282
Some(ty) => format!("<super: <class '{}'>, <{} object>>", type_name, ty.name()),
258283
None => format!("<super: <class '{type_name}'>, NULL>"),

‎crates/vm/src/builtins/type.rs‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1721,6 +1721,7 @@ impl Py<PyType> {
17211721
}
17221722

17231723
#[pyclass(
1724+
itemsize = core::mem::size_of::<crate::builtins::descriptor::PyMemberDefLayout>(),
17241725
with(
17251726
Py,
17261727
Constructor,
@@ -2598,14 +2599,10 @@ impl Constructor for PyType {
25982599
tuple.try_into_typed(vm)?
25992600
};
26002601

2601-
// Check if base has itemsize > 0 - can't add arbitrary slots to variable-size types
2602-
// Types like int, bytes, tuple have itemsize > 0 and don't allow custom slots
2603-
// But types like weakref.ref have itemsize = 0 and DO allow slots
2604-
let has_custom_slots = slots
2605-
.as_slice()
2606-
.iter()
2607-
.any(|s| !matches!(s.as_bytes(), b"__dict__" | b"__weakref__"));
2608-
if has_custom_slots && base.slots.itemsize > 0 {
2602+
// Any nonempty __slots__ is rejected when the base has a variable
2603+
// item size, including a tuple of only `__dict__` or `__weakref__`.
2604+
// Types like weakref.ref have itemsize 0 and do allow slots.
2605+
if !slots.as_slice().is_empty() && base.slots.itemsize > 0 {
26092606
return Err(vm.new_type_error(format!(
26102607
"nonempty __slots__ not supported for subtype of '{}'",
26112608
base.name()
@@ -2722,7 +2719,9 @@ impl Constructor for PyType {
27222719
// Add HAS_WEAKREF if:
27232720
// 1. __slots__ is not defined (automatic weakref support), OR
27242721
// 2. __weakref__ is in __slots__
2725-
let may_add_weakref = !base.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF);
2722+
// A variable-size base does not gain a weakref slot.
2723+
let may_add_weakref =
2724+
base.slots.itemsize == 0 && !base.slots.flags.has_feature(PyTypeFlags::HAS_WEAKREF);
27262725
if (heaptype_slots.is_none() && may_add_weakref) || add_weakref {
27272726
flags |= PyTypeFlags::HAS_WEAKREF | PyTypeFlags::MANAGED_WEAKREF;
27282727
}

‎crates/vm/src/class.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,10 +120,16 @@ pub trait StaticType {
120120
where
121121
Self: PyClassImpl,
122122
{
123+
// inherit_special COPYVAL(tp_itemsize): the direct base, and only when
124+
// this type left the slot at 0. The base type object already exists.
125+
let mut slots = Self::make_slots();
126+
if slots.itemsize == 0 {
127+
slots.itemsize = Self::static_baseclass().slots.itemsize;
128+
}
123129
PyType::new_static(
124130
Self::static_baseclass().to_owned(),
125131
Default::default(),
126-
Self::make_slots(),
132+
slots,
127133
Self::static_metaclass().to_owned(),
128134
)
129135
.unwrap()

0 commit comments

Comments
 (0)