From 41e693a29a13b7dac62f3b5894e31a1e1008c043 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 8 Aug 2026 22:16:22 +0900 Subject: [PATCH 01/26] coroutine: preserve yield-from and exhausted state --- pyre/pyre-interpreter/src/baseobjspace.rs | 36 +++++++++++++------ pyre/pyre-interpreter/src/eval.rs | 25 ++++++++++++- .../src/jitcode_dispatch/mod.rs | 6 ++++ 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 71f07b4de45..322acd7baa2 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -16966,10 +16966,16 @@ fn generator_send_ex( w_arg: PyObjectRef, operr: Option, throw_args: Option<([PyObjectRef; 3], usize)>, + closing: bool, ) -> PyResult { use pyre_object::generator::*; unsafe { if w_generator_is_exhausted(gen_obj) { + if is_coroutine(gen_obj) && !closing { + return Err(PyError::runtime_error( + "cannot reuse already awaited coroutine", + )); + } if let Some(err) = operr { return Err(err); } @@ -16983,6 +16989,11 @@ fn generator_send_ex( let frame_ptr = w_generator_get_frame(gen_obj) as *mut crate::pyframe::PyFrame; if frame_ptr.is_null() { w_generator_set_exhausted(gen_obj); + if is_coroutine(gen_obj) && !closing { + return Err(PyError::runtime_error( + "cannot reuse already awaited coroutine", + )); + } if let Some(err) = operr { return Err(err); } @@ -17057,7 +17068,7 @@ pub(crate) fn resume_yield_from( Some(err) => throw_yield_from(w_yf, err, throw_args), None if unsafe { pyre_object::is_none(w_arg) } => { if unsafe { pyre_object::generator::is_generator_or_coroutine(w_yf) } { - generator_send_ex(w_yf, w_none(), None, None) + generator_send_ex(w_yf, w_none(), None, None, false) } else { next(w_yf) } @@ -17104,7 +17115,7 @@ fn throw_yield_from( ) -> PyResult { unsafe { if pyre_object::generator::is_generator_or_coroutine(w_yf) { - return generator_send_ex(w_yf, w_none(), Some(err), throw_args); + return generator_send_ex(w_yf, w_none(), Some(err), throw_args, false); } } let throw = match getattr_str(w_yf, "throw") { @@ -17128,7 +17139,7 @@ fn close_yield_from(w_yf: PyObjectRef) -> PyResult { unsafe { if pyre_object::generator::is_generator_or_coroutine(w_yf) { let exit = PyError::new(PyErrorKind::GeneratorExit, String::new()); - return match generator_send_ex(w_yf, w_none(), Some(exit), None) { + return match generator_send_ex(w_yf, w_none(), Some(exit), None, true) { Ok(_) => Err(PyError::runtime_error(format!( "{} ignored GeneratorExit", generator_kind(w_yf) @@ -17248,7 +17259,7 @@ unsafe fn leak_generator_iteration(mut e: PyError, message: &str) -> PyError { /// PyPy: GeneratorIterator.next() — equivalent to __next__ fn generator_next(gen_obj: PyObjectRef) -> PyResult { - generator_send_ex(gen_obj, w_none(), None, None) + generator_send_ex(gen_obj, w_none(), None, None, false) } /// __next__ method wrapper @@ -17408,7 +17419,7 @@ pub(crate) fn generator_send_method(args: &[PyObjectRef]) -> PyResult { args[0] }; let value = if args.len() > 1 { args[1] } else { w_none() }; - generator_send_ex(gen_obj, value, None, None) + generator_send_ex(gen_obj, value, None, None, false) } /// PyPy: GeneratorIterator.descr_throw(w_type, w_val=None, w_tb=None) @@ -17486,6 +17497,7 @@ fn generator_throw_impl(args: &[PyObjectRef], warn_legacy_signature: bool) -> Py w_none(), Some(err), Some(([w_type, w_val, w_tb], argc)), + false, ); } }; @@ -17494,6 +17506,7 @@ fn generator_throw_impl(args: &[PyObjectRef], warn_legacy_signature: bool) -> Py w_none(), Some(err), Some(([w_type, w_val, w_tb], argc)), + false, ) } @@ -17525,7 +17538,7 @@ pub(crate) fn generator_close_method(args: &[PyObjectRef]) -> PyResult { } } let err = PyError::new(PyErrorKind::GeneratorExit, String::new()); - let mut result = match generator_send_ex(gen_obj, w_none(), Some(err), None) { + let mut result = match generator_send_ex(gen_obj, w_none(), Some(err), None, true) { Ok(_) => { // Generator yielded after GeneratorExit — RuntimeError. // generator.py:267-268 `"%s ignored GeneratorExit" % self.KIND`. @@ -17579,14 +17592,14 @@ pub(crate) fn coroutine_await_method(args: &[PyObjectRef]) -> PyResult { pub(crate) fn coroutine_wrapper_next_method(args: &[PyObjectRef]) -> PyResult { let wrapper = args.first().copied().unwrap_or(PY_NULL); let coroutine = unsafe { pyre_object::generator::w_coroutine_wrapper_get_coroutine(wrapper) }; - generator_send_ex(coroutine, w_none(), None, None) + generator_send_ex(coroutine, w_none(), None, None, false) } pub(crate) fn coroutine_wrapper_send_method(args: &[PyObjectRef]) -> PyResult { let wrapper = args.first().copied().unwrap_or(PY_NULL); let coroutine = unsafe { pyre_object::generator::w_coroutine_wrapper_get_coroutine(wrapper) }; let value = crate::type_methods::arg_or_none(args, 1); - generator_send_ex(coroutine, value, None, None) + generator_send_ex(coroutine, value, None, None, false) } pub(crate) fn coroutine_wrapper_throw_method(args: &[PyObjectRef]) -> PyResult { @@ -17730,7 +17743,7 @@ fn async_gen_asend_do_send(awaitable: PyObjectRef, mut arg: PyObjectRef) -> PyRe } unsafe { w_async_generator_set_running(async_gen, true) }; } - let result = generator_send_ex(async_gen, arg, None, None) + let result = generator_send_ex(async_gen, arg, None, None, false) .and_then(|value| async_gen_unwrap_value(async_gen, value)); if result.is_err() { unsafe { w_async_generator_set_running(async_gen, false) }; @@ -17770,6 +17783,7 @@ pub(crate) fn async_gen_asend_close_method(args: &[PyObjectRef]) -> PyResult { w_none(), Some(PyError::new(PyErrorKind::GeneratorExit, String::new())), None, + true, ); unsafe { pyre_object::generator::w_async_generator_set_running(async_gen, false) }; match result { @@ -17907,6 +17921,7 @@ fn async_gen_athrow_do_send(awaitable: PyObjectRef, arg: PyObjectRef) -> PyResul w_none(), Some(PyError::new(PyErrorKind::GeneratorExit, String::new())), None, + true, ) } else { // `AsyncGenerator.athrow` already warned from the caller-visible @@ -17915,7 +17930,7 @@ fn async_gen_athrow_do_send(awaitable: PyObjectRef, arg: PyObjectRef) -> PyResul generator_throw_impl(&[async_gen, exc_type, exc_value, exc_tb], false) } } else { - generator_send_ex(async_gen, arg, None, None) + generator_send_ex(async_gen, arg, None, None, false) }; let result = match result { Ok(value) => { @@ -17968,6 +17983,7 @@ pub(crate) fn async_gen_athrow_close_method(args: &[PyObjectRef]) -> PyResult { w_none(), Some(PyError::new(PyErrorKind::GeneratorExit, String::new())), None, + true, ); unsafe { pyre_object::generator::w_async_generator_set_running(async_gen, false) }; match result { diff --git a/pyre/pyre-interpreter/src/eval.rs b/pyre/pyre-interpreter/src/eval.rs index ebd371e614b..f684abc1ad3 100644 --- a/pyre/pyre-interpreter/src/eval.rs +++ b/pyre/pyre-interpreter/src/eval.rs @@ -4358,7 +4358,30 @@ impl OpcodeStepExecutor for PyFrame { // ── yield from / send ── fn get_yield_from_iter(&mut self) -> Result<(), PyError> { let iterable = self.pop(); - let iter = crate::baseobjspace::iter(iterable)?; + // CPython 3.14 `GET_YIELD_FROM_ITER` / PyPy's coroutine-aware + // `YIELD_FROM`: exact generators already are their iterator. A + // native coroutine is also sent to directly, but only when the + // current frame is itself a coroutine or was marked by + // `types.coroutine` with CO_ITERABLE_COROUTINE. Calling ordinary + // `iter()` here loses both halves of that distinction because native + // coroutine objects intentionally expose no public `__iter__`. + let iter = unsafe { + if pyre_object::generator::is_coroutine(iterable) { + let flags = self.code().flags; + if !flags + .intersects(crate::CodeFlags::COROUTINE | crate::CodeFlags::ITERABLE_COROUTINE) + { + return Err(PyError::type_error( + "cannot 'yield from' a coroutine object in a non-coroutine generator", + )); + } + iterable + } else if pyre_object::generator::is_generator(iterable) { + iterable + } else { + crate::baseobjspace::iter(iterable)? + } + }; self.push(iter); Ok(()) } diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs index 96f15241254..4b238c1326e 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs @@ -2749,6 +2749,12 @@ pub fn step( ctx: &mut WalkContext<'_, '_, Sym>, ) -> Result<(DispatchOutcome, usize), DispatchError> { let op: DecodedOp = decode_op_at(code, pc).ok_or(DispatchError::UndecodableOpcode { pc })?; + // The walker mixes translated vable operations (which update the shadow) + // with concrete interpreter steps (which update the heap PyFrame). Pull + // those concrete writes into `virtualizable_boxes` before any handler can + // read or synchronize it, matching the invariant documented by + // `TraceCtx::refresh_virtualizable_shadow_from_heap`. + ctx.trace_ctx.refresh_virtualizable_shadow_from_heap(); if ctx.is_top_level { ctx.session.borrow_mut().recording_opcode_position = op.pc; } From 37dfc23ae83209b685644239b2befd9a404f0044 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 8 Aug 2026 22:27:29 +0900 Subject: [PATCH 02/26] copy: reject types without constructors --- pyre/pyre-interpreter/src/reduce_protocol.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pyre/pyre-interpreter/src/reduce_protocol.rs b/pyre/pyre-interpreter/src/reduce_protocol.rs index 94fea2dc217..7848ffb10a7 100644 --- a/pyre/pyre-interpreter/src/reduce_protocol.rs +++ b/pyre/pyre-interpreter/src/reduce_protocol.rs @@ -306,6 +306,20 @@ pub fn descr_reduce_ex(w_obj: PyObjectRef, proto: i64) -> PyResult { } } if proto >= 2 { + // CPython 3.14 `typeobject.c:reduce_newobj`: `tp_new == NULL` + // means there is no constructor with which `__newobj__` can rebuild + // the instance. Pyre represents that slot state with + // `Py_TPFLAGS_DISALLOW_INSTANTIATION`; check it before collecting new + // arguments, exactly as `reduce_newobj` does. Coroutine objects and + // their `__await__` wrappers both have this shape. + let w_type = crate::typedef::r#type(w_obj) + .ok_or_else(|| PyError::type_error("cannot determine type for __reduce_ex__"))?; + if unsafe { pyre_object::w_type_disallows_instantiation(w_type.as_ptr()) } { + return Err(PyError::type_error(format!( + "cannot pickle '{}' object", + typename(w_obj) + ))); + } let (hasargs, w_args, w_kwargs) = getnewargs(w_obj)?; // objectobject.py:276 / `_PyObject_GetState(required)`: a type whose // instances carry C-level state that `__dict__`/`__slots__` cannot From bb63f8f98eec5ae11dd5652e9a539261f18d8dc1 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 8 Aug 2026 22:38:11 +0900 Subject: [PATCH 03/26] async-for: chain invalid anext awaitables --- pyre/pyre-interpreter/src/eval.rs | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/pyre/pyre-interpreter/src/eval.rs b/pyre/pyre-interpreter/src/eval.rs index f684abc1ad3..fab0ad040fa 100644 --- a/pyre/pyre-interpreter/src/eval.rs +++ b/pyre/pyre-interpreter/src/eval.rs @@ -4496,14 +4496,29 @@ impl OpcodeStepExecutor for PyFrame { )) })?; let next = crate::call::call_function_impl_result(method, &[])?; - let awaitable = crate::baseobjspace::get_awaitable_iter(next, 0).map_err(|err| { - if err.kind == crate::PyErrorKind::TypeError { - crate::PyError::type_error(format!( - "'async for' received an invalid object from __anext__: {}", - crate::type_methods::arg_type_name(next) - )) - } else { - err + let awaitable = crate::baseobjspace::get_awaitable_iter(next, 0).map_err(|mut cause| { + // CPython 3.14 `_PyEval_GetANext` uses + // `_PyErr_FormatFromCause` for *every* failure produced while + // converting `__anext__`'s result to an awaitable. In + // particular, an exception raised by `result.__await__()` is the + // explicit cause of this TypeError; only an exception raised by + // `__anext__` itself propagates unchanged above. + let message = format!( + "'async for' received an invalid object from __anext__: {}", + crate::type_methods::arg_type_name(next) + ); + let cause_obj = cause.to_exc_object(); + let _roots = pyre_object::gc_roots::push_roots(); + pyre_object::gc_roots::pin_root(cause_obj); + let cause_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + let mut error = crate::PyError::type_error(message); + let error_obj = error.to_exc_object(); + let cause_obj = pyre_object::gc_roots::shadow_stack_get(cause_slot); + unsafe { + pyre_object::interp_exceptions::w_exception_set_context(error_obj, cause_obj); + pyre_object::interp_exceptions::w_exception_set_cause(error_obj, cause_obj); + pyre_object::interp_exceptions::w_exception_set_suppress_context(error_obj, true); + crate::PyError::from_exc_object(error_obj) } })?; self.push(awaitable); From 47204b08b4bfa8d1fd8e3309cefbb3d9e543aaa5 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sat, 8 Aug 2026 23:55:45 +0900 Subject: [PATCH 04/26] coroutine: report unawaited finalizer errors --- pyre/pyre-interpreter/src/baseobjspace.rs | 67 ++++++++++++++----- pyre/pyre-interpreter/src/eval.rs | 14 ++-- pyre/pyre-interpreter/src/executioncontext.rs | 40 +++++++++++ pyre/pyre-interpreter/src/pyframe.rs | 40 ++++++++--- pyre/pyre-interpreter/src/typedef.rs | 10 ++- pyre/pyre-jit/src/eval.rs | 7 +- 6 files changed, 143 insertions(+), 35 deletions(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 322acd7baa2..6768b611b00 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -16883,9 +16883,11 @@ unsafe fn generator_frame_is_finished(gen_obj: PyObjectRef, frame: &mut crate::p unsafe { w_generator_set_exhausted(gen_obj) }; frame.set_frame_finished_execution(true); frame.w_yielding_from = PY_NULL; - // The exhausted flag makes `descr_clear` take its permitted finalized - // branch. It has no semantic failure path after that state transition. - let _ = frame.descr_clear(); + // Internal generator completion clears the owned frame directly. Public + // `frame.clear()` instead calls the generator finalizer first; routing + // completion through it would report a normally closed coroutine as + // never awaited. + frame.clear_references(); frame.f_backref = std::ptr::null_mut(); frame.f_generator_nowref = PY_NULL; unsafe { w_generator_set_frame(gen_obj, std::ptr::null_mut()) }; @@ -18052,6 +18054,51 @@ pub(crate) fn async_gen_athrow_throw_method(args: &[PyObjectRef]) -> PyResult { /// is collected. If the suspended frame is still live and its current instruction is /// covered by an exception-table handler (a `finally`/`except`/`with` cleanup), raise /// GeneratorExit into it so the cleanup runs. +fn warn_unawaited_coroutine(gen_obj: PyObjectRef) { + // CPython 3.14 `_PyErr_WarnUnawaitedCoroutine`: the public `warnings` + // module owns the overridable formatting hook. A hook failure is + // unraisable from finalizer context, and only a RuntimeWarning raised by + // the warning filter counts as already warned; every other failure gets a + // direct warning fallback as well. + let repr = unsafe { crate::display::py_repr_wtf8(gen_obj) } + .unwrap_or_else(|_| Wtf8Buf::from_string("".to_string())); + let where_desc = + crate::display::wtf8_format!("Exception ignored while finalizing coroutine ", repr); + + let hook_result = crate::importing::get_sys_module("warnings").and_then(|warnings| { + match findattr_result(warnings, "_warn_unawaited_coroutine") { + Ok(Some(hook)) => Some(crate::call::call_function_impl_result(hook, &[gen_obj])), + Ok(None) => None, + Err(err) => Some(Err(err)), + } + }); + let warned = match hook_result { + Some(Ok(_)) => true, + Some(Err(mut err)) => { + let exc = err.to_exc_object(); + let is_runtime_warning = crate::builtins::lookup_exc_class("RuntimeWarning") + .is_some_and(|cls| unsafe { isinstance_w(exc, cls) }); + err.write_unraisable(w_none(), &where_desc, gen_obj); + is_runtime_warning + } + None => false, + }; + + if !warned { + let qualname = unsafe { pyre_object::generator::w_generator_get_qualname(gen_obj) }; + let qualname = if qualname.is_null() { + Wtf8Buf::from_string("".to_string()) + } else { + unsafe { pyre_object::w_str_get_wtf8(qualname) }.to_wtf8_buf() + }; + let message = crate::display::wtf8_format!("coroutine '", qualname, "' was never awaited"); + let w_message = pyre_object::w_str_from_wtf8(message); + if let Err(mut err) = crate::warn::warn_category_w(w_message, "RuntimeWarning", 1) { + err.write_unraisable(w_none(), &where_desc, gen_obj); + } + } +} + pub fn generator_finalize(gen_obj: PyObjectRef) -> PyResult { unsafe { use pyre_object::generator::*; @@ -18071,20 +18118,8 @@ pub fn generator_finalize(gen_obj: PyObjectRef) -> PyResult { { w_coroutine_set_warned_unawaited(gen_obj); let _roots = pyre_object::gc_roots::push_roots(); - let root_base = pyre_object::gc_roots::shadow_stack_len(); pyre_object::gc_roots::pin_root(gen_obj); - let w_mod = crate::importing::get_sys_module("_warnings") - .ok_or_else(|| PyError::runtime_error("_warnings is not initialized"))?; - pyre_object::gc_roots::pin_root(w_mod); - let w_f = getattr_str( - pyre_object::gc_roots::shadow_stack_get(root_base + 1), - "_warn_unawaited_coroutine", - )?; - pyre_object::gc_roots::pin_root(w_f); - crate::call::call_function_impl_result( - pyre_object::gc_roots::shadow_stack_get(root_base + 2), - &[pyre_object::gc_roots::shadow_stack_get(root_base)], - )?; + warn_unawaited_coroutine(gen_obj); } if last_instr < 0 { return Ok(w_none()); // not started — cannot be inside a handler diff --git a/pyre/pyre-interpreter/src/eval.rs b/pyre/pyre-interpreter/src/eval.rs index fab0ad040fa..fc93618db4f 100644 --- a/pyre/pyre-interpreter/src/eval.rs +++ b/pyre/pyre-interpreter/src/eval.rs @@ -1032,8 +1032,12 @@ pub unsafe fn walk_pyframe_roots_area( let locals_slot = &mut (*(frame)).locals_cells_stack_w as *mut *mut pyre_object::FixedObjectArray; visitor(&mut *(locals_slot as *mut majit_ir::GcRef)); - let gen_slot = &mut (*(frame)).f_generator_nowref as *mut PyObjectRef; - visitor(&mut *(gen_slot as *mut majit_ir::GcRef)); + // pyframe.py:75-76/276-279: translated PyPy stores the + // generator owner in `f_generator_wref`; the `_nowref` + // fallback exists only when translation has no weakrefs. + // This field is therefore a non-owning back-reference in + // pyre and must not keep the generator alive through an + // escaped `cr_frame`/`gi_frame`. let yielding_slot = &mut (*(frame)).w_yielding_from as *mut PyObjectRef; visitor(&mut *(yielding_slot as *mut majit_ir::GcRef)); // pyframe.py:115-116 `self.builtin = ...` — the picked @@ -1382,8 +1386,10 @@ pub fn walk_suspended_generator_frame( } } - let gen_slot = &mut (*frame).f_generator_nowref as *mut PyObjectRef; - visitor(&mut *(gen_slot as *mut majit_ir::GcRef)); + // `f_generator_nowref` is the raw counterpart of PyPy's translated + // `f_generator_wref`, not a frame-owned GC edge (pyframe.py:75-76, + // 276-279). The generator owns this suspended frame in the other + // direction. let yielding_slot = &mut (*frame).w_yielding_from as *mut PyObjectRef; visitor(&mut *(yielding_slot as *mut majit_ir::GcRef)); diff --git a/pyre/pyre-interpreter/src/executioncontext.rs b/pyre/pyre-interpreter/src/executioncontext.rs index 47245ee1c46..7ea2a3a85fb 100644 --- a/pyre/pyre-interpreter/src/executioncontext.rs +++ b/pyre/pyre-interpreter/src/executioncontext.rs @@ -830,6 +830,15 @@ impl ExecutionContext { self._run_finalizers_now(); } + /// Request the CPython refcount boundary associated with exposing a + /// coroutine's frame. The action runs before the next opcode, after the + /// attribute receiver has left the value stack. + pub fn finalize_discarded_coroutine_after_frame_get(&mut self) { + if !self.user_del_action.is_null() { + unsafe { (*self.user_del_action).collect_oldgen_and_fire() }; + } + } + /// pypy/interpreter/executioncontext.py:185-200 `run_trace_func`. /// /// ```python @@ -2212,6 +2221,12 @@ pub struct UserDelAction { pub finalizers_lock_count: usize, pub enabled_at_app_level: bool, pub pending_with_disabled_del: Option>, + /// Run one old-generation reachability pass immediately before the next + /// finalizer drain. CPython's temporary coroutine decref can make + /// `f().cr_frame` warn before the following opcode; pyre's tracing GC + /// requests the equivalent pass only after LOAD_ATTR has replaced the + /// coroutine with its escaped frame on the value stack. + collect_oldgen_before_run: bool, /// `pypy/interpreter/executioncontext.py:640` — /// `self.space.finalizer_queue` access target. /// @@ -2255,6 +2270,7 @@ impl UserDelAction { finalizers_lock_count: 0, enabled_at_app_level: true, pending_with_disabled_del: None, + collect_oldgen_before_run: false, finalizer_queue: WRootFinalizerQueue, }); let action_ptr: *mut dyn AsyncActionOps = &mut *action; @@ -2294,6 +2310,14 @@ impl UserDelAction { } } + /// Defer the reachability pass until the next opcode boundary. Calling + /// it inside the `cr_frame` getter would still see the getter argument as + /// a root and could not observe a discarded temporary coroutine. + pub fn collect_oldgen_and_fire(&mut self) { + self.collect_oldgen_before_run = true; + self.fire(); + } + pub fn gc_disabled(&mut self, w_obj: PyObjectRef) -> bool { let _ = w_obj; if let Some(list) = self.pending_with_disabled_del.as_mut() { @@ -2337,6 +2361,18 @@ impl UserDelAction { current(), ); } + // pyframe.py:75-76/276-279 stores this back-reference as + // `f_generator_wref` in translated PyPy. The collector has + // already declared `current()` dead, so clear pyre's raw + // representation before the finalizer queue releases its last + // temporary root. Explicit `frame.clear()` also calls + // `generator_finalize`, but must retain this association while + // its still-live generator owns the frame. + let frame = + unsafe { pyre_object::generator::w_generator_get_frame(current()) } as *mut PyFrame; + if !frame.is_null() && unsafe { (*frame).f_generator_nowref == current() } { + unsafe { (*frame).f_generator_nowref = pyre_object::PY_NULL }; + } return; } let Some(w_type) = crate::typedef::r#type(current()) else { @@ -2380,6 +2416,10 @@ impl AsyncActionOps for UserDelAction { _executioncontext: &mut ExecutionContext, _frame: *mut PyFrame, ) -> Result<(), crate::PyError> { + if self.collect_oldgen_before_run { + self.collect_oldgen_before_run = false; + pyre_object::gc_hook::try_gc_collect_oldgen(); + } self._run_finalizers(); Ok(()) } diff --git a/pyre/pyre-interpreter/src/pyframe.rs b/pyre/pyre-interpreter/src/pyframe.rs index 6c7e131f408..293bdbc24d7 100644 --- a/pyre/pyre-interpreter/src/pyframe.rs +++ b/pyre/pyre-interpreter/src/pyframe.rs @@ -3260,18 +3260,18 @@ impl PyFrame { /// frame. Refuses on an executing (non-generator) frame or a running /// generator (`"cannot clear an executing frame"`) and on a generator /// frame suspended at a `yield` (`"cannot clear a suspended frame"`); - /// a not-yet-started or already-exhausted generator is finalized - /// (marked exhausted). Otherwise clears `w_f_trace`, resets + /// a not-yet-started or already-exhausted generator is finalized. + /// Otherwise clears `w_f_trace`, resets /// `w_locals` to a fresh dict, and replaces every local / cell / free /// var / stack slot (cells are rebound to fresh empty cells so a /// shared inner/outer cell is not mutated). pub fn descr_clear(&mut self) -> Result<(), crate::PyError> { - if !self.frame_finished_execution() { - if !self._is_generator_or_coroutine() { - return Err(crate::PyError::runtime_error( - "cannot clear an executing frame", - )); - } + // CPython 3.14 `frameobject.c:frame_clear_impl`: a frame owned by a + // generator/coroutine is finalized and then returned from directly. + // In particular, clearing a never-started coroutine frame must run + // the unawaited-coroutine warning while the owner is still reachable; + // merely marking it exhausted loses that finalizer event. + if self._is_generator_or_coroutine() { let w_gen = self.get_generator(); if !w_gen.is_null() { if unsafe { pyre_object::generator::w_generator_is_running(w_gen) } { @@ -3291,11 +3291,30 @@ impl PyFrame { "cannot clear a suspended frame", )); } - // Not started or already exhausted: finalize. - unsafe { pyre_object::generator::w_generator_set_exhausted(w_gen) }; + crate::baseobjspace::generator_finalize(w_gen)?; + return Ok(()); } + // pyframe.py:815-820: a dead `f_generator_wref` simply skips the + // generator finalizer and proceeds to clear the escaped frame. + // It is not an executing non-generator frame. + self.clear_references(); + return Ok(()); + } + if !self.frame_finished_execution() { + return Err(crate::PyError::runtime_error( + "cannot clear an executing frame", + )); } + self.clear_references(); + Ok(()) + } + + /// Clear the frame-owned reference region after its owner has already + /// completed. This is the internal `_PyFrame_ClearExceptCode` half used + /// by generator completion; unlike public `frame.clear()`, it must not + /// re-enter `_PyGen_Finalize` on the generator that is doing the clearing. + pub(crate) fn clear_references(&mut self) { if let Some(debug) = self.getdebug() { let had_locals = !debug.w_locals.is_null(); // Allocate before remembering debugdata: a minor can happen here. @@ -3322,7 +3341,6 @@ impl PyFrame { self.set_locals_w(i, w_newvalue); } self.valuestackdepth = 0; - Ok(()) } /// pyframe.py:773 fget_f_lasti → space.newint(self.last_instr) diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index e2ba5f2dff7..bd307defc21 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -25443,7 +25443,15 @@ fn coroutine_get_suspended(args: &[PyObjectRef]) -> crate::PyResult { coroutine_getter(args, 1) } fn coroutine_get_frame(args: &[PyObjectRef]) -> crate::PyResult { - coroutine_getter(args, 2) + let frame = coroutine_getter(args, 2)?; + // CPython 3.14 releases a temporary coroutine immediately after + // `f().cr_frame`; schedule pyre's tracing-GC equivalent for the next + // opcode, when the getter receiver is no longer rooted by LOAD_ATTR. + let ec = crate::call::getexecutioncontext() as *mut crate::executioncontext::ExecutionContext; + if !ec.is_null() { + unsafe { (*ec).finalize_discarded_coroutine_after_frame_get() }; + } + Ok(frame) } fn coroutine_get_code(args: &[PyObjectRef]) -> crate::PyResult { coroutine_getter(args, 3) diff --git a/pyre/pyre-jit/src/eval.rs b/pyre/pyre-jit/src/eval.rs index 59cb7a3b258..6fc74525020 100644 --- a/pyre/pyre-jit/src/eval.rs +++ b/pyre/pyre-jit/src/eval.rs @@ -1112,8 +1112,10 @@ unsafe fn memoryview_object_destructor(obj_addr: usize) { /// items in place: barrier-less interpreter stores require that at minors, /// and it is harmless duplicate marking at majors. A stationary /// `std::alloc` block always forwards its items in place. -/// - `f_generator_nowref`, `w_yielding_from`, `w_builtin`, `w_globals` -/// — the ref-bearing statics. +/// - `w_yielding_from`, `w_builtin`, `w_globals` — the ref-bearing statics. +/// `f_generator_nowref` is excluded: it is the raw counterpart of PyPy's +/// translated `f_generator_wref` (pyframe.py:75-76/276-279), hence a +/// non-owning back-reference rather than a GC edge. /// - `debugdata` / `lastblock` — managed field slots are forwarded. /// - `debugdata->{w_locals, w_f_trace, hidden_operationerr}` — null-guarded. /// @@ -1186,7 +1188,6 @@ unsafe fn pyframe_object_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut ma } } - f(&mut frame.f_generator_nowref as *mut pyre_object::PyObjectRef as *mut majit_ir::GcRef); f(&mut frame.w_yielding_from as *mut pyre_object::PyObjectRef as *mut majit_ir::GcRef); f(&mut frame.w_builtin as *mut pyre_object::PyObjectRef as *mut majit_ir::GcRef); f(&mut frame.w_globals as *mut pyre_object::PyObjectRef as *mut majit_ir::GcRef); From 9be3e5ffa34f556a138735249baf5038fde5d93e Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 00:32:25 +0900 Subject: [PATCH 05/26] str: port CPython sizeof reporting --- pyre/pyre-interpreter/src/module/sys/vm.rs | 114 ++++++++++++++------- pyre/pyre-interpreter/src/typedef.rs | 41 ++++++++ 2 files changed, 118 insertions(+), 37 deletions(-) diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index ea6d22ee897..b06f1edb5fa 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -10,39 +10,68 @@ use crate::{ use pyre_object::*; use std::sync::OnceLock; -const GETSIZEOF_MISSING: &str = r#"getsizeof(...) - getsizeof(object, default) -> int - - Return the size of object in bytes. - -sys.getsizeof(object, default) will always return default on PyPy, and -raise a TypeError if default is not provided. - -First note that the CPython documentation says that this function may -raise a TypeError, so if you are seeing it, it means that the program -you are using is not correctly handling this case. - -On PyPy, though, it always raises TypeError. Before looking for -alternatives, please take a moment to read the following explanation as -to why it is the case. What you are looking for may not be possible. - -A memory profiler using this function is most likely to give results -inconsistent with reality on PyPy. It would be possible to have -sys.getsizeof() return a number (with enough work), but that may or -may not represent how much memory the object uses. It doesn't even -make really sense to ask how much *one* object uses, in isolation -with the rest of the system. For example, instances have maps, -which are often shared across many instances; in this case the maps -would probably be ignored by an implementation of sys.getsizeof(), -but their overhead is important in some cases if they are many -instances with unique maps. Conversely, equal strings may share -their internal string data even if they are different objects---or -empty containers may share parts of their internals as long as they -are empty. Even stranger, some lists create objects as you read -them; if you try to estimate the size in memory of range(10**6) as -the sum of all items' size, that operation will by itself create one -million integer objects that never existed in the first place. -"#; +const GETSIZEOF_DOC: &str = "getsizeof(object [, default]) -> int\n\n\ +Return the size of object in bytes."; + +/// CPython 3.14 `_PySys_GetSizeOf`: look up `__sizeof__` on the type, +/// call the bound method, require a non-negative Py_ssize_t result, then add +/// the pre-header used by a heap type. Pyre has no physical CPython object +/// pre-header, but exposes the 3.14 logical size required by `sys.getsizeof`. +fn get_sizeof(w_obj: PyObjectRef) -> crate::PyResult { + let roots = pyre_object::gc_roots::push_roots(); + let obj_slot = roots.base(); + roots.pin_root(w_obj); + let current = || roots.get(obj_slot); + let method = unsafe { crate::baseobjspace::lookup_special(current(), "__sizeof__")? } + .ok_or_else(|| { + crate::PyError::type_error(format!( + "Type {} doesn't define __sizeof__", + crate::baseobjspace::object_functionstr_type_name(current()), + )) + })?; + let w_size = crate::call::call_function_impl_result(method, &[])?; + + // PyLong_AsSsize_t accepts bool/int subclasses but performs no __index__ + // conversion on an arbitrary return value. + let size = if unsafe { pyre_object::is_bool(w_size) } { + unsafe { pyre_object::w_bool_get_value(w_size) as i64 } + } else if unsafe { pyre_object::is_int(w_size) } { + unsafe { pyre_object::w_int_get_value(w_size) } + } else if unsafe { pyre_object::is_long(w_size) } { + let value = unsafe { pyre_object::w_long_get_value(w_size) }; + if pyre_object::longobject::jit_bigint_to_i64_fits(value) == 0 { + return Err(crate::PyError::overflow_error( + "Python int too large to convert to C ssize_t", + )); + } + pyre_object::longobject::jit_bigint_to_i64_value(value) + } else { + return Err(crate::PyError::type_error("an integer is required")); + }; + if size < 0 { + return Err(crate::PyError::value_error( + "__sizeof__() should return >= 0", + )); + } + + // `_PyType_PreHeaderSize(Py_TYPE(o))`: on 64-bit CPython a managed heap + // instance has a 16-byte GC header plus a 16-byte managed dict/weakref + // prefix (half those sizes on 32-bit). This unit covers heap instances and + // untracked str; tracked builtin types will extend the type-layout port. + let pre_header = crate::typedef::r#type(current()) + .filter(|tp| unsafe { pyre_object::w_type_is_heaptype(tp.as_ptr()) }) + .map_or(0u64, |_| (4 * std::mem::size_of::()) as u64); + let total = (size as u64) + .checked_add(pre_header) + .expect("Py_ssize_t plus the fixed pre-header fits in size_t"); + if total <= i64::MAX as u64 { + Ok(w_int_new(total as i64)) + } else { + Ok(pyre_object::w_long_new( + pyre_object::rbigint::RBigInt::from_u128(total as u128), + )) + } +} /// Shared stub type for `sys._getframe`, `sys.stdout` and other module-level /// sys attributes that expose attribute bags. @@ -2274,13 +2303,24 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { &[true, false], "getsizeof", )?; + let w_obj = scope[0]; let w_default = scope[1]; - if w_default.is_null() { - return Err(crate::PyError::type_error(GETSIZEOF_MISSING)); + let roots = pyre_object::gc_roots::push_roots(); + let obj_slot = roots.base(); + roots.pin_root(w_obj); + let default_slot = obj_slot + 1; + roots.pin_root(w_default); + match get_sizeof(roots.get(obj_slot)) { + Ok(size) => Ok(size), + Err(err) + if !w_default.is_null() && err.kind == crate::PyErrorKind::TypeError => + { + Ok(roots.get(default_slot)) + } + Err(err) => Err(err), } - Ok(w_default) }, - GETSIZEOF_MISSING, + GETSIZEOF_DOC, ), ); // PyPy normally omits CPython's raw refcount API. The shared ctypes diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index bd307defc21..56dc68a48f9 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -5270,6 +5270,47 @@ fn init_str_type(ns: PyObjectRef) { ), ) }; + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__sizeof__", + crate::gateway::make_builtin_function_with_arity_and_text_signature( + "__sizeof__", + |args| { + crate::type_methods::arity_no_args(args, "__sizeof__")?; + let value = pyre_object::w_str_get_wtf8(args[0]); + let len = value.code_points().count(); + let maxchar = value.code_points().map(|cp| cp.to_u32()).max().unwrap_or(0); + let kind = if maxchar < 0x100 { + 1usize + } else if maxchar < 0x10000 { + 2 + } else { + 4 + }; + let word = std::mem::size_of::(); + // unicodeobject.c:unicode_sizeof_impl. Exact ASCII uses + // PyASCIIObject (5 words), exact non-ASCII uses + // PyCompactUnicodeObject (7 words), and Unicode + // subclasses use the two-block PyUnicodeObject (8 words). + let base = + if pyre_object::pyobject::is_exact_type(args[0], &pyre_object::STR_TYPE) { + if maxchar < 0x80 { 5 * word } else { 7 * word } + } else { + 8 * word + }; + let size = + base.checked_add((len + 1).checked_mul(kind).ok_or_else(|| { + crate::PyError::overflow_error("string is too large") + })?) + .ok_or_else(|| crate::PyError::overflow_error("string is too large"))?; + Ok(w_int_new(size as i64)) + }, + 1, + "($self, /)", + ), + ) + }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, From 859aa630a36e301aa5e2103b80d746c7df891d0b Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 01:04:36 +0900 Subject: [PATCH 06/26] int: port CPython size metadata --- pyre/pyre-interpreter/src/typedef.rs | 111 +++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index 56dc68a48f9..d81ab4f024b 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -10239,6 +10239,36 @@ fn make_getset_property_full( ) } +/// Logical CPython 3.14 `tp_basicsize` / `tp_itemsize` values ported so far. +/// These belong to the type object, not to its Python namespace: CPython's +/// `type_members` exposes both through read-only data descriptors. +fn cpython_type_layout(w_type: PyObjectRef) -> Option<(i64, i64)> { + if std::ptr::eq(w_type, w_object()) { + return Some(((2 * std::mem::size_of::()) as i64, 0)); + } + let int_type = gettypefor(&pyre_object::INT_TYPE)?.as_ptr(); + if std::ptr::eq(w_type, int_type) + || unsafe { crate::baseobjspace::issubtype_w(w_type, int_type) } + { + return Some(((3 * std::mem::size_of::()) as i64, 4)); + } + None +} + +fn type_basicsize_getter(args: &[PyObjectRef]) -> Result { + let Some((basic, _)) = cpython_type_layout(args[1]) else { + return Err(crate::PyError::attribute_error("__basicsize__")); + }; + Ok(w_int_new(basic)) +} + +fn type_itemsize_getter(args: &[PyObjectRef]) -> Result { + let Some((_, item)) = cpython_type_layout(args[1]) else { + return Err(crate::PyError::attribute_error("__itemsize__")); + }; + Ok(w_int_new(item)) +} + fn init_type_type(ns: PyObjectRef) { // type.__new__(metatype, name, bases, dict) — creates new type unsafe { @@ -10419,6 +10449,22 @@ fn init_type_type(ns: PyObjectRef) { ), ) }; + // CPython 3.14 `typeobject.c:type_members`: these are read-only member + // descriptors on `type`, rather than ordinary entries inherited through + // the inspected class's MRO. + for (name, function) in [ + ("__basicsize__", type_basicsize_getter as DunderFn), + ("__itemsize__", type_itemsize_getter as DunderFn), + ] { + let getter = make_builtin_function_with_arity(name, function, 2); + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + name, + make_getset_property_named(getter, PY_NULL, PY_NULL, name), + ) + }; + } // typeobject.py:833-841 `W_TypeObject.descr_or` / `descr_ror` delegate // to `_pypy_generic_alias._create_union`. unsafe { @@ -16864,6 +16910,22 @@ fn long_bit_length(value: &BigInt) -> Result { } } +/// CPython 3.14 `longobject.c:int___sizeof___impl` counts base-2**30 +/// `digit` cells, independently of pyre's internal RBigInt digit width. +fn int_cpython_digit_count(w_obj: PyObjectRef) -> Result { + let bits = if unsafe { pyre_object::is_bool(w_obj) } { + i64::from(unsafe { pyre_object::w_bool_get_value(w_obj) }) + } else if unsafe { pyre_object::is_int(w_obj) } { + let magnitude = unsafe { pyre_object::w_int_get_value(w_obj) }.unsigned_abs(); + i64::from(u64::BITS - magnitude.leading_zeros()) + } else if unsafe { pyre_object::is_long(w_obj) } { + long_bit_length(unsafe { pyre_object::w_long_get_value(w_obj) })? + } else { + 0 + }; + Ok(if bits == 0 { 1 } else { (bits - 1) / 30 + 1 }) +} + /// `intobject.py:657 W_IntObject.descr_bit_length` / /// `longobject.py:48 W_AbstractLongObject.descr_bit_length`, exposed through /// `interpindirect2app` (`intobject.py:1171`). @@ -16958,6 +17020,27 @@ fn init_int_type(ns: PyObjectRef) { ), ) }; + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__sizeof__", + crate::gateway::make_builtin_function_with_arity_and_text_signature( + "__sizeof__", + |args| { + crate::type_methods::arity_no_args(args, "__sizeof__")?; + let ndigits = int_cpython_digit_count(args[0])?; + let w_type = crate::typedef::r#type(args[0]) + .expect("every int has a type") + .as_ptr(); + let (basicsize, itemsize) = cpython_type_layout(w_type) + .expect("int and its subclasses have CPython layout metadata"); + Ok(w_int_new(basicsize + itemsize * ndigits)) + }, + 1, + "($self, /)", + ), + ) + }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, @@ -18828,6 +18911,34 @@ fn init_object_type(ns: PyObjectRef) { ), ) }; + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__sizeof__", + // CPython 3.14 `typeobject.c:object___sizeof___impl`: add the + // live type's basicsize and its variable item contribution. The + // latter is currently non-zero for the CPython-layout `int` port. + crate::gateway::make_builtin_function_with_arity_and_text_signature( + "__sizeof__", + |args| { + crate::type_methods::arity_no_args(args, "__sizeof__")?; + let w_type = crate::typedef::r#type(args[0]) + .expect("every Python object has a type") + .as_ptr(); + let (basicsize, itemsize) = cpython_type_layout(w_type) + .unwrap_or((2 * std::mem::size_of::() as i64, 0)); + let nitems = if itemsize == 0 { + 0 + } else { + int_cpython_digit_count(args[0])? + }; + Ok(w_int_new(basicsize + itemsize * nitems)) + }, + 1, + "($self, /)", + ), + ) + }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, From 7d29c633ac5ab1ac4b72288da823b74a2a86c08e Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 01:36:13 +0900 Subject: [PATCH 07/26] type: expose CPython layout metadata --- pyre/pyre-interpreter/src/call.rs | 38 ++++---- pyre/pyre-interpreter/src/typedef.rs | 139 +++++++++++++++++++++++++-- 2 files changed, 148 insertions(+), 29 deletions(-) diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index 004cf39d518..0b8a2d5f854 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -5145,6 +5145,18 @@ pub unsafe fn create_all_slots( } else { (*base_layout).nslots }; + // CPython 3.14 `type_new_slots`: a variable-sized base may add a + // managed instance dict, but may not add weakrefs or any explicit + // `__slots__` entry. These are the three variable layouts currently + // exposed by pyre's builtin type registry. + let base_has_variable_items = if base_layout.is_null() { + false + } else { + let typedef = (*base_layout).typedef; + std::ptr::eq(typedef, &pyre_object::INT_TYPE) + || std::ptr::eq(typedef, &pyre_object::TUPLE_TYPE) + || std::ptr::eq(typedef, &pyre_object::bytesobject::BYTES_TYPE) + }; // typeobject.py:1150-1204 create_all_slots let mut newslotnames = Vec::new(); @@ -5154,6 +5166,12 @@ pub unsafe fn create_all_slots( wantdict = false; wantweakref = false; let all_names = collect_slot_names(w_slots)?; + if base_has_variable_items && !all_names.is_empty() { + return Err(crate::PyError::type_error(format!( + "nonempty __slots__ not supported for subtype of '{}'", + pyre_object::w_type_get_name(w_bestbase) + ))); + } if !all_names.iter().any(|name| name == "__doc__") && !crate::type_dict_contains(w_type, "__doc__") { @@ -5195,24 +5213,6 @@ pub unsafe fn create_all_slots( // typeobject.py:1178: string_sort(newslotnames) newslotnames.sort(); - // CPython 3.14 rejects additional instance slots on the remaining - // variable-size builtin layouts. `str` is deliberately excluded: - // current CPython permits them (configparser._Line relies on it), - // and PyPy stores them in BaseUserClassMapdict like every other - // app-level subclass slot. - if !newslotnames.is_empty() && !base_layout.is_null() { - let typedef = (*base_layout).typedef; - if std::ptr::eq(typedef, &pyre_object::INT_TYPE) - || std::ptr::eq(typedef, &pyre_object::TUPLE_TYPE) - || std::ptr::eq(typedef, &pyre_object::bytesobject::BYTES_TYPE) - { - return Err(crate::PyError::type_error(format!( - "nonempty __slots__ not supported for subtype of '{}'", - pyre_object::w_type_get_name(w_bestbase) - ))); - } - } - // typeobject.py:1183-1189: create_slot loop let type_name = pyre_object::w_type_get_name(w_type); let mut slot_index = base_nslots; @@ -5257,7 +5257,7 @@ pub unsafe fn create_all_slots( } else { // typeobject.py:1151-1153: no __slots__ wantdict = true; - wantweakref = true; + wantweakref = !base_has_variable_items; } // PyPy dict subclasses are W_DictMultiObject instances, so their diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index d81ab4f024b..dfc7a9e6ea9 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -822,7 +822,12 @@ pub fn init_typeobjects() { ); // slice — PyPy: sliceobject.py, bases=(object,) - let slice_type = new_typeobject_with_base("slice", init_slice_type, object_type); + let slice_type = new_typeobject_with_base_and_layout( + "slice", + init_slice_type, + object_type, + &pyre_object::sliceobject::SLICE_TYPE as *const PyType, + ); unsafe { pyre_object::w_type_set_acceptable_as_base_class(slice_type, false) }; reg.insert( &pyre_object::sliceobject::SLICE_TYPE as *const PyType as usize, @@ -1026,17 +1031,23 @@ pub fn init_typeobjects() { ); // rangeobject.c PyRange_Type carries no Py_TPFLAGS_BASETYPE, so // `range` is not an acceptable base class. - let range_type = new_typeobject_with_base("range", init_range_type, object_type); + let range_type = new_typeobject_with_base_and_layout( + "range", + init_range_type, + object_type, + &pyre_object::functional::RANGE_TYPE as *const PyType, + ); unsafe { pyre_object::w_type_set_acceptable_as_base_class(range_type, false) }; reg.insert( &pyre_object::functional::RANGE_TYPE as *const PyType as usize, range_type as usize, ); // memoryobject.py:731 W_MemoryView.typedef.acceptable_as_base_class = False - let memoryview_type = new_typeobject_with_base( + let memoryview_type = new_typeobject_with_base_and_layout( "memoryview", crate::builtins::init_memoryview_type, object_type, + &pyre_object::memoryview::MEMORYVIEW_TYPE as *const PyType, ); unsafe { pyre_object::w_type_set_acceptable_as_base_class(memoryview_type, false); @@ -10243,16 +10254,108 @@ fn make_getset_property_full( /// These belong to the type object, not to its Python namespace: CPython's /// `type_members` exposes both through read-only data descriptors. fn cpython_type_layout(w_type: PyObjectRef) -> Option<(i64, i64)> { - if std::ptr::eq(w_type, w_object()) { - return Some(((2 * std::mem::size_of::()) as i64, 0)); + if w_type.is_null() || !unsafe { pyre_object::is_type(w_type) } { + return None; } - let int_type = gettypefor(&pyre_object::INT_TYPE)?.as_ptr(); - if std::ptr::eq(w_type, int_type) - || unsafe { crate::baseobjspace::issubtype_w(w_type, int_type) } + let word = std::mem::size_of::() as i64; + let layout = unsafe { pyre_object::w_type_get_layout(w_type) }; + let is = |candidate: *const PyType| std::ptr::eq(layout, candidate); + let (base, item) = if is(&pyre_object::INSTANCE_TYPE) { + (2 * word, 0) + } else if is(&pyre_object::TYPE_TYPE) { + (117 * word, 5 * word) + } else if is(&pyre_object::INT_TYPE) + || is(&pyre_object::LONG_TYPE) + || is(&pyre_object::BOOL_TYPE) + { + (3 * word, 4) + } else if is(&pyre_object::FLOAT_TYPE) { + (3 * word, 0) + } else if is(&pyre_object::COMPLEX_TYPE) { + (4 * word, 0) + } else if is(&pyre_object::STR_TYPE) { + (8 * word, 0) + } else if is(&pyre_object::bytesobject::BYTES_TYPE) { + (4 * word + 1, 1) + } else if is(&pyre_object::bytearrayobject::BYTEARRAY_TYPE) { + (7 * word, 0) + } else if is(&pyre_object::LIST_TYPE) { + (5 * word, 0) + } else if is(&pyre_object::TUPLE_TYPE) { + (4 * word, word) + } else if is(&pyre_object::DICT_TYPE) { + (6 * word, 0) + } else if is(&pyre_object::setobject::SET_TYPE) || is(&pyre_object::setobject::FROZENSET_TYPE) { + (25 * word, 0) + } else if is(&pyre_object::functional::RANGE_TYPE) { + (6 * word, 0) + } else if is(&pyre_object::sliceobject::SLICE_TYPE) { + (5 * word, 0) + } else if is(&pyre_object::memoryview::MEMORYVIEW_TYPE) { + (18 * word, word) + } else if is(&pyre_object::functional::MAP_TYPE) { + (5 * word, 0) + } else if is(&pyre_object::functional::FILTER_TYPE) + || is(&pyre_object::functional::REVERSED_TYPE) { - return Some(((3 * std::mem::size_of::()) as i64, 4)); + (4 * word, 0) + } else if is(&pyre_object::functional::ZIP_TYPE) { + (6 * word, 0) + } else if is(&pyre_object::functional::ENUMERATE_TYPE) { + (7 * word, 0) + } else { + return None; + }; + // PyPy typeobject.py:103-129 keeps the total slot count on Layout, whose + // typedef identifies the fixed builtin prefix. CPython appends one pointer + // per user slot to that same prefix. + let mut slots = unsafe { pyre_object::w_type_get_nslots(w_type) } as i64; + // A dict subclass is represented by a composed instance in pyre and owns + // one private `__dict_data__` Layout slot for its mapping payload. CPython + // keeps that payload in the fixed PyDictObject prefix, so the private slot + // must not contribute to the public `tp_basicsize` projection. + let mut current = unsafe { pyre_object::w_type_get_layout_ptr(w_type) }; + while !current.is_null() { + if unsafe { + (*current) + .newslotnames + .iter() + .any(|name| name == "__dict_data__") + } { + slots -= 1; + break; + } + current = unsafe { (*current).base_layout }; } - None + Some((base + slots * word, item)) +} + +fn cpython_type_offsets(w_type: PyObjectRef) -> Option<(i64, i64)> { + cpython_type_layout(w_type)?; + let word = std::mem::size_of::() as i64; + let layout = unsafe { pyre_object::w_type_get_layout(w_type) }; + let is = |candidate: *const PyType| std::ptr::eq(layout, candidate); + let (mut dict, mut weakref) = if is(&pyre_object::TYPE_TYPE) { + (33 * word, 46 * word) + } else if is(&pyre_object::setobject::SET_TYPE) || is(&pyre_object::setobject::FROZENSET_TYPE) { + (0, 24 * word) + } else if is(&pyre_object::memoryview::MEMORYVIEW_TYPE) { + (0, 17 * word) + } else { + (0, 0) + }; + // Python 3.14 managed dict/weakref storage lives in the negative + // pre-header. Preserve a builtin's positive inline offset when it already + // owns the slot; otherwise heap types use the managed sentinel/offset. + if unsafe { pyre_object::w_type_is_heaptype(w_type) } { + if dict == 0 && unsafe { pyre_object::w_type_get_hasdict(w_type) } { + dict = -1; + } + if weakref == 0 && unsafe { pyre_object::w_type_get_weakrefable(w_type) } { + weakref = -4 * word; + } + } + Some((dict, weakref)) } fn type_basicsize_getter(args: &[PyObjectRef]) -> Result { @@ -10269,6 +10372,20 @@ fn type_itemsize_getter(args: &[PyObjectRef]) -> Result Result { + let Some((dict, _)) = cpython_type_offsets(args[1]) else { + return Err(crate::PyError::attribute_error("__dictoffset__")); + }; + Ok(w_int_new(dict)) +} + +fn type_weakrefoffset_getter(args: &[PyObjectRef]) -> Result { + let Some((_, weakref)) = cpython_type_offsets(args[1]) else { + return Err(crate::PyError::attribute_error("__weakrefoffset__")); + }; + Ok(w_int_new(weakref)) +} + fn init_type_type(ns: PyObjectRef) { // type.__new__(metatype, name, bases, dict) — creates new type unsafe { @@ -10455,6 +10572,8 @@ fn init_type_type(ns: PyObjectRef) { for (name, function) in [ ("__basicsize__", type_basicsize_getter as DunderFn), ("__itemsize__", type_itemsize_getter as DunderFn), + ("__dictoffset__", type_dictoffset_getter as DunderFn), + ("__weakrefoffset__", type_weakrefoffset_getter as DunderFn), ] { let getter = make_builtin_function_with_arity(name, function, 2); unsafe { From a9a8ce9a835752ee39de350f7712ca291ccf7f7f Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 02:07:47 +0900 Subject: [PATCH 08/26] bytearray: port CPython allocation metadata --- pyre/pyre-interpreter/src/baseobjspace.rs | 18 +++++ pyre/pyre-interpreter/src/eval.rs | 36 +++++++++ .../src/module/_ctypes/interp_ctypes.rs | 6 +- .../src/module/_io/bytesio.rs | 5 ++ .../src/objspace/descroperation.rs | 2 + pyre/pyre-interpreter/src/typedef.rs | 79 +++++++++++++++---- pyre/pyre-object/src/bytearrayobject.rs | 56 +++++++++++-- 7 files changed, 181 insertions(+), 21 deletions(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 6768b611b00..2a97a429a8e 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -4025,11 +4025,19 @@ unsafe fn setitem_bytearray_slice( crate::builtins::bytearray_check_exports(obj)?; } let vec = pyre_object::bytearrayobject::w_bytearray_vec_mut(obj); + let old_size = vec.len(); if step == 1 { let cur = vec.len(); let s = (start.max(0) as usize).min(cur); let e = (stop.max(start) as usize).min(cur).max(s); + if s == 0 && sequence2.len() < e - s { + pyre_object::bytearrayobject::w_bytearray_advance_logical_start( + obj, + e - s - sequence2.len(), + ); + } vec.splice(s..e, sequence2.iter().copied()); + pyre_object::bytearrayobject::w_bytearray_sync_alloc(obj, old_size); return Ok(w_none()); } // Extended slice: `descr_setitem` forbids resizing — the source length @@ -19133,10 +19141,15 @@ pub(crate) fn delitem_slot(obj: PyObjectRef, index: PyObjectRef) -> Result<(), P let (start, stop, step, slicelength) = crate::sliceobject::slice_adjust_indices(rs, rp, st, len); let vec = pyre_object::bytearrayobject::w_bytearray_vec_mut(obj); + let old_size = vec.len(); if step == 1 { let s = start.max(0) as usize; let e = stop.max(start).min(vec.len() as i64) as usize; + if s == 0 { + pyre_object::bytearrayobject::w_bytearray_advance_logical_start(obj, e - s); + } vec.drain(s..e); + pyre_object::bytearrayobject::w_bytearray_sync_alloc(obj, old_size); return Ok(()); } let mut indices: Vec = Vec::with_capacity(slicelength as usize); @@ -19156,13 +19169,18 @@ pub(crate) fn delitem_slot(obj: PyObjectRef, index: PyObjectRef) -> Result<(), P vec.remove(idx as usize); } } + pyre_object::bytearrayobject::w_bytearray_sync_alloc(obj, old_size); return Ok(()); } let i = subscript_index_w("bytearray", index)?; let len = pyre_object::bytearrayobject::w_bytearray_len(obj) as i64; let idx = if i < 0 { len + i } else { i }; if idx >= 0 && idx < len { + if idx == 0 { + pyre_object::bytearrayobject::w_bytearray_advance_logical_start(obj, 1); + } pyre_object::bytearrayobject::w_bytearray_vec_mut(obj).remove(idx as usize); + pyre_object::bytearrayobject::w_bytearray_sync_alloc(obj, len as usize); return Ok(()); } return Err(PyError::new( diff --git a/pyre/pyre-interpreter/src/eval.rs b/pyre/pyre-interpreter/src/eval.rs index fc93618db4f..2fac5183a54 100644 --- a/pyre/pyre-interpreter/src/eval.rs +++ b/pyre/pyre-interpreter/src/eval.rs @@ -8237,6 +8237,42 @@ result = ( } } + #[test] + fn test_bytearray_cpython_allocation_and_sizeof() { + let source = "\ +value = bytearray() +allocations = [] +for item in range(20): + value.append(item) + allocations.append(value.__alloc__()) + +del value[:5] +after_prefix_delete = value.__alloc__() +for _ in range(7): + value.append(0) + +class Sub(bytearray): + pass +sub = Sub(b'abc') + +value.clear() +result = ( + allocations == [2, 5, 5, 5, 8, 8, 8, 12, 12, 12, 12, + 19, 19, 19, 19, 19, 19, 19, 27, 27] + and after_prefix_delete == 27 + and value.__alloc__() == 1 + and sub.__alloc__() == 4 + and sub.__sizeof__() == Sub.__basicsize__ + sub.__alloc__() +) +"; + let (res, frame) = run_exec_frame(source); + res.expect("CPython bytearray allocation metadata failed"); + unsafe { + let result = w_dict_getitem_str(frame.w_globals, "result").unwrap(); + assert!(crate::baseobjspace::is_true(result).unwrap()); + } + } + #[test] fn test_percent_c_uses_fully_qualified_type_name() { let source = "\ diff --git a/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs b/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs index a08bec57b10..4151b5422a1 100644 --- a/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs +++ b/pyre/pyre-interpreter/src/module/_ctypes/interp_ctypes.rs @@ -517,7 +517,11 @@ fn ctypes_resize( } let size = requested as usize; if let Some(ba) = cdata::cdata_buffer(obj) { - unsafe { pyre_object::w_bytearray_vec_mut(ba).resize(size, 0) }; + unsafe { + let old_size = pyre_object::w_bytearray_len(ba); + pyre_object::w_bytearray_vec_mut(ba).resize(size, 0); + pyre_object::bytearrayobject::w_bytearray_sync_alloc(ba, old_size); + }; } Ok(pyre_object::w_none()) } diff --git a/pyre/pyre-interpreter/src/module/_io/bytesio.rs b/pyre/pyre-interpreter/src/module/_io/bytesio.rs index 0138fc96e45..a94fbf71bbd 100644 --- a/pyre/pyre-interpreter/src/module/_io/bytesio.rs +++ b/pyre/pyre-interpreter/src/module/_io/bytesio.rs @@ -156,9 +156,11 @@ impl W_BytesIO { } if self.pos == AT_END { let vec = unsafe { pyre_object::bytearrayobject::w_bytearray_vec_mut(self.buffer) }; + let old_len = vec.len(); vec.try_reserve_exact(data.len()) .map_err(|_| crate::PyError::memory_error(""))?; vec.extend_from_slice(data); + unsafe { pyre_object::bytearrayobject::w_bytearray_sync_alloc(self.buffer, old_len) }; return Ok(data.len() as i64); } @@ -180,6 +182,7 @@ impl W_BytesIO { vec.resize(p, 0); } vec.resize(end, 0); + unsafe { pyre_object::bytearrayobject::w_bytearray_sync_alloc(self.buffer, old_len) }; } vec[p..end].copy_from_slice(data); self.pos = if end > old_len { AT_END } else { end as i64 }; @@ -190,9 +193,11 @@ impl W_BytesIO { // rpython/rlib/rStringIO.py:178-200 never enlarges and always seeks // to the resulting end using the AT_END sentinel. let vec = unsafe { pyre_object::bytearrayobject::w_bytearray_vec_mut(self.buffer) }; + let old_len = vec.len(); if size < vec.len() as i64 { vec.truncate(size as usize); } + unsafe { pyre_object::bytearrayobject::w_bytearray_sync_alloc(self.buffer, old_len) }; self.pos = AT_END; } diff --git a/pyre/pyre-interpreter/src/objspace/descroperation.rs b/pyre/pyre-interpreter/src/objspace/descroperation.rs index 939a995bfad..27ca9547170 100644 --- a/pyre/pyre-interpreter/src/objspace/descroperation.rs +++ b/pyre/pyre-interpreter/src/objspace/descroperation.rs @@ -2214,6 +2214,7 @@ pub(crate) unsafe fn bytearray_inplace_repeat( } if count == 0 { pyre_object::bytearrayobject::w_bytearray_vec_mut(ba).clear(); + pyre_object::bytearrayobject::w_bytearray_sync_alloc(ba, len); return Ok(()); } if len == 0 { @@ -2226,6 +2227,7 @@ pub(crate) unsafe fn bytearray_inplace_repeat( for _ in 1..count { vec.extend_from_slice(&snapshot); } + pyre_object::bytearrayobject::w_bytearray_sync_alloc(ba, len); Ok(()) } diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index dfc7a9e6ea9..ce2bae1641f 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -19644,7 +19644,10 @@ fn bytearray_descr_init_value( Ok(item) => { let byte = crate::baseobjspace::byte_w(item, "byte")?; crate::builtins::bytearray_check_exports(target)?; - pyre_object::bytearrayobject::w_bytearray_vec_mut(target).push(byte); + let vec = pyre_object::bytearrayobject::w_bytearray_vec_mut(target); + let old_size = vec.len(); + vec.push(byte); + pyre_object::bytearrayobject::w_bytearray_sync_alloc(target, old_size); } Err(e) if e.kind == crate::PyErrorKind::StopIteration => break, Err(e) => return Err(e), @@ -22937,6 +22940,7 @@ fn bytearray_method_imul(args: &[PyObjectRef]) -> Result Result Result Result Result Result vec.remove(pos), + Some(pos) => { + let old_size = vec.len(); + vec.remove(pos); + pyre_object::bytearrayobject::w_bytearray_sync_alloc(args[0], old_size); + } None => { return Err(crate::PyError::value_error("value not found in bytearray")); } @@ -23084,7 +23103,9 @@ fn bytearray_method_pop(args: &[PyObjectRef]) -> Result Result Result Result { crate::type_methods::require_receiver(args, "__init__")?; unsafe { - if !pyre_object::bytesobject::bytes_like_data(args[0]).is_empty() { + let old_size = pyre_object::bytearrayobject::w_bytearray_len(args[0]); + if old_size != 0 { crate::builtins::bytearray_check_exports(args[0])?; pyre_object::bytearrayobject::w_bytearray_vec_mut(args[0]).clear(); + pyre_object::bytearrayobject::w_bytearray_sync_alloc(args[0], old_size); } } let fresh = bytearray_descr_init_value(args, args[0])?; @@ -23139,7 +23165,9 @@ fn bytearray_descr_init(args: &[PyObjectRef]) -> Result Result Result { crate::type_methods::arity_slot(args, 0)?; let capacity = unsafe { pyre_object::bytearrayobject::w_bytearray_capacity(args[0]) }; - // PyPy's resizable list includes its trailing NUL. CPython 3.14 exposes - // the same convention: empty has alloc 0, otherwise payload capacity + 1. - Ok(w_int_new(if capacity == 0 { - 0 - } else { - (capacity + 1) as i64 - })) + Ok(w_int_new(capacity as i64)) +} + +fn bytearray_descr_sizeof(args: &[PyObjectRef]) -> Result { + crate::type_methods::arity_slot(args, 0)?; + // CPython 3.14 `bytearray_sizeof_impl`: `_PyObject_SIZE(Py_TYPE(self))` + // plus the exposed `ob_alloc` byte count. + let basicsize = cpython_type_layout( + crate::typedef::r#type(args[0]) + .map(|tp| tp.as_ptr()) + .unwrap_or_else(|| gettypeobject(&pyre_object::bytearrayobject::BYTEARRAY_TYPE)), + ) + .expect("bytearray and its subclasses have CPython layout metadata") + .0; + let alloc = unsafe { pyre_object::bytearrayobject::w_bytearray_capacity(args[0]) }; + Ok(w_int_new(basicsize + alloc as i64)) } fn bytearray_descr_resize(args: &[PyObjectRef]) -> Result { @@ -23220,11 +23257,13 @@ fn bytearray_descr_resize(args: &[PyObjectRef]) -> Result old_size { vec.try_reserve_exact(new_size - old_size) .map_err(|_| crate::PyError::memory_error(""))?; } vec.resize(new_size, 0); + pyre_object::bytearrayobject::w_bytearray_sync_alloc(args[0], previous_size); } Ok(w_none()) } @@ -23265,6 +23304,18 @@ fn init_bytearray_type(ns: PyObjectRef) { make_builtin_function("__init__", bytearray_descr_init), ) }; + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__sizeof__", + crate::gateway::make_builtin_function_with_arity_and_text_signature( + "__sizeof__", + bytearray_descr_sizeof, + 1, + "($self, /)", + ), + ) + }; for (name, function, arity) in [ ("__repr__", bytearray_descr_repr as DunderFn, 1), ("__str__", bytearray_descr_repr, 1), diff --git a/pyre/pyre-object/src/bytearrayobject.rs b/pyre/pyre-object/src/bytearrayobject.rs index fe116d5e1cb..c740f583b53 100644 --- a/pyre/pyre-object/src/bytearrayobject.rs +++ b/pyre/pyre-object/src/bytearrayobject.rs @@ -13,6 +13,15 @@ pub static BYTEARRAY_TYPE: PyType = crate::pyobject::new_pytype("bytearray"); pub struct W_BytearrayObject { pub ob_header: PyObject, pub data: *mut Vec, + /// CPython `PyByteArrayObject.ob_alloc`, including the trailing NUL byte. + /// + /// This cannot be derived from `Vec::capacity()`: Rust's allocator uses a + /// different growth policy, while `bytearray.__alloc__()` and + /// `bytearray.__sizeof__()` expose CPython's logical allocation directly. + pub alloc: usize, + /// Logical `ob_start - ob_bytes` offset. pyre keeps the payload itself at + /// `Vec[0]`, but preserves this allocation state for prefix slice deletes. + pub logical_offset: usize, /// `_exports` — count of active buffer exports. Size-changing mutators /// are refused while this is positive (`_check_exports`). pub exports: i64, @@ -47,6 +56,7 @@ impl crate::lltype::GcType for W_BytearrayObject { /// `dont_look_inside` public constructors below, so the tracer never reaches it /// (the `Vec`-by-value argument never crosses a residual call ABI). fn w_bytearray_alloc(buf: Vec) -> PyObjectRef { + let alloc = if buf.is_empty() { 0 } else { buf.len() + 1 }; let data = crate::gc_storage::gc_alloc_storage_box(buf, crate::bytesobject::bytes_data_gc_type_id()); let header = PyObject { @@ -62,6 +72,8 @@ fn w_bytearray_alloc(buf: Vec) -> PyObjectRef { W_BytearrayObject { ob_header: header, data, + alloc, + logical_offset: 0, exports: 0, w_dict: PY_NULL, w_weakreflifeline: PY_NULL, @@ -73,6 +85,8 @@ fn w_bytearray_alloc(buf: Vec) -> PyObjectRef { crate::lltype::malloc_typed(W_BytearrayObject { ob_header: header, data, + alloc, + logical_offset: 0, exports: 0, w_dict: PY_NULL, w_weakreflifeline: PY_NULL, @@ -128,6 +142,8 @@ pub fn w_bytearray_subclass_from_bytes(bytes: &[u8], w_class: PyObjectRef) -> Py w_class: crate::gc_roots::shadow_stack_get(root_base), }, data: crate::lltype::malloc_raw(bytes.to_vec()), + alloc: if bytes.is_empty() { 0 } else { bytes.len() + 1 }, + logical_offset: 0, exports: 0, w_dict: PY_NULL, w_weakreflifeline: PY_NULL, @@ -178,16 +194,41 @@ pub unsafe fn w_bytearray_len(obj: PyObjectRef) -> usize { } } -/// Number of payload bytes currently reserved by the backing storage. -/// `bytearrayobject.py:604 descr_alloc` reports the resizable list allocation; -/// pyre's `Vec` is the equivalent storage object. +/// CPython 3.14 `PyByteArrayObject.ob_alloc`, including the trailing NUL. pub unsafe fn w_bytearray_capacity(obj: PyObjectRef) -> usize { + unsafe { (*(obj as *const W_BytearrayObject)).alloc } +} + +/// Port of CPython 3.14 `bytearray_resize_lock_held`'s allocation policy. +pub unsafe fn w_bytearray_sync_alloc(obj: PyObjectRef, old_size: usize) { unsafe { - let ba = &*(obj as *const W_BytearrayObject); - (*ba.data).capacity() + let ba = &mut *(obj as *mut W_BytearrayObject); + let size = (*ba.data).len(); + if size == old_size { + return; + } + let current = ba.alloc; + let fits = size + ba.logical_offset + 1 <= current; + if fits && size >= current / 2 { + return; + } + ba.alloc = if fits { + size + 1 + } else if size <= current + (current >> 3) { + size + (size >> 3) + if size < 9 { 3 } else { 6 } + } else { + size + 1 + }; + ba.logical_offset = 0; } } +/// CPython `bytearray_setslice_linear`: a shrinking prefix slice advances +/// `ob_start` before entering the resize policy. +pub unsafe fn w_bytearray_advance_logical_start(obj: PyObjectRef, amount: usize) { + unsafe { (*(obj as *mut W_BytearrayObject)).logical_offset += amount } +} + pub unsafe fn w_bytearray_getitem(obj: PyObjectRef, index: usize) -> u8 { unsafe { let ba = &*(obj as *const W_BytearrayObject); @@ -220,7 +261,9 @@ pub unsafe fn w_bytearray_find(obj: PyObjectRef, value: u8, start: usize) -> i64 pub unsafe fn w_bytearray_extend(obj: PyObjectRef, other: &[u8]) { unsafe { let ba = &mut *(obj as *mut W_BytearrayObject); + let old_size = (*ba.data).len(); (*ba.data).extend_from_slice(other); + w_bytearray_sync_alloc(obj, old_size); } } @@ -243,7 +286,8 @@ pub unsafe fn w_bytearray_data_mut(obj: PyObjectRef) -> &'static mut [u8] { /// Get a mutable reference to the backing `Vec`, for length-changing /// mutators (append / insert / remove / pop / clear). Caller must -/// ensure the bytearray is not aliased while the reference is live. +/// ensure the bytearray is not aliased while the reference is live and call +/// [`w_bytearray_sync_alloc`] after every actual length change. pub unsafe fn w_bytearray_vec_mut(obj: PyObjectRef) -> &'static mut Vec { unsafe { let ba = &*(obj as *const W_BytearrayObject); From c08027d047b298be0c108de900abdf0c635bef59 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 02:52:01 +0900 Subject: [PATCH 09/26] list: port CPython allocation metadata --- pyre/pyre-interpreter/src/baseobjspace.rs | 8 + pyre/pyre-interpreter/src/builtins.rs | 54 ++--- pyre/pyre-interpreter/src/eval.rs | 109 ++++++++++ .../src/objspace/descroperation.rs | 3 +- pyre/pyre-interpreter/src/type_methods.rs | 49 ++++- pyre/pyre-interpreter/src/typedef.rs | 31 +++ .../src/jitcode_dispatch/fbw_state.rs | 13 +- .../src/jitcode_dispatch/mod.rs | 5 + .../src/jitcode_dispatch/residual_call.rs | 26 ++- .../src/jitcode_dispatch/specialize.rs | 3 +- .../src/jitcode_dispatch/tests.rs | 13 +- pyre/pyre-object/src/float_array.rs | 2 +- pyre/pyre-object/src/int_array.rs | 2 +- pyre/pyre-object/src/listobject.rs | 192 +++++++++++++++++- 14 files changed, 446 insertions(+), 64 deletions(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 2a97a429a8e..10a6bd6cf1a 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -19078,6 +19078,7 @@ pub(crate) fn delitem_slot(obj: PyObjectRef, index: PyObjectRef) -> Result<(), P // Extended-slice delete: gather the selected indices, then // pop them in descending order so earlier removals do not // shift the positions of later targets. + let old_allocated = w_list_allocated(obj); let mut indices: Vec = Vec::with_capacity(slicelength as usize); let mut i = start; for n in 0..slicelength { @@ -19094,6 +19095,13 @@ pub(crate) fn delitem_slot(obj: PyObjectRef, index: PyObjectRef) -> Result<(), P w_list_pop(obj, idx); } } + if slicelength > 0 { + pyre_object::listobject::w_list_finish_batch_resize( + obj, + len as usize, + old_allocated, + ); + } return Ok(()); } // `descr_delitem`: getindex_w(idx, IndexError, "list") coerces a diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index 41a26d0f256..828f29313ae 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -10006,34 +10006,19 @@ pub(crate) fn builtin_list_ctor(args: &[PyObjectRef]) -> Result = (0..n) - .filter_map(|i| w_list_getitem(obj, i as i64)) - .collect(); - return Ok(w_list_new(items)); - } - if is_exact_tuple(obj) { - let n = w_tuple_len(obj); - let items: Vec<_> = (0..n) - .filter_map(|i| w_tuple_getitem(obj, i as i64)) - .collect(); - return Ok(w_list_new(items)); - } - } - // listobject.py:1049-1053 `_extend_from_iterable` asks for the source's - // length hint before obtaining/consuming its iterator. The hint is only a - // preallocation aid, but RuntimeError and other non-TypeError failures - // from `__len__` / `__length_hint__` are observable and must propagate. - let _ = crate::baseobjspace::length_hint(obj, 0)?; - // Consume iterator — PyPy: listobject.py W_ListObject(iterable) - Ok(w_list_new(collect_iterable(obj)?)) + // CPython `list_vectorcall_impl` allocates an empty list then delegates to + // the same `list_extend` machinery as `list.__init__`. Keep both the source + // and result rooted across that allocation and the iterator callbacks. + let _roots = pyre_object::gc_roots::push_roots(); + let root_base = pyre_object::gc_roots::shadow_stack_len(); + pyre_object::gc_roots::pin_root(args[0]); + let list = w_list_new(vec![]); + pyre_object::gc_roots::pin_root(list); + crate::type_methods::list_method_extend(&[ + pyre_object::gc_roots::shadow_stack_get(root_base + 1), + pyre_object::gc_roots::shadow_stack_get(root_base), + ])?; + Ok(pyre_object::gc_roots::shadow_stack_get(root_base + 1)) } /// The message-less `MemoryError` an unsatisfiable reservation raises. @@ -13290,7 +13275,7 @@ pub(crate) fn builtin_sorted(args: &[PyObjectRef]) -> Result Result String { } } +fn list_descr_sizeof(args: &[PyObjectRef]) -> Result { + crate::type_methods::arity_slot(args, 0)?; + let list = crate::type_methods::require_list_receiver(args, "__sizeof__", false)?; + // CPython 3.14 `list___sizeof___impl`: dynamic `tp_basicsize` plus one + // pointer-sized word for every logically allocated item slot. + let w_type = crate::typedef::r#type(list) + .map(|tp| tp.as_ptr()) + .unwrap_or_else(|| gettypeobject(&pyre_object::LIST_TYPE)); + let basicsize = cpython_type_layout(w_type) + .expect("list and its subclasses have CPython layout metadata") + .0; + let allocated = unsafe { pyre_object::listobject::w_list_allocated(list) }; + // `list_sort_impl` temporarily writes -1. CPython performs this expression + // in `size_t`, so the unsigned wrap makes an exact base list report 32. + let size = (basicsize as usize) + .wrapping_add((allocated as usize).wrapping_mul(std::mem::size_of::())); + Ok(w_int_new(size as i64)) +} + fn init_list_type(ns: PyObjectRef) { // listobject.py W_ListObject.typedef, kept in source order. unsafe { @@ -4839,6 +4858,18 @@ fn init_list_type(ns: PyObjectRef) { ) }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy(ns, "__hash__", w_none()) }; + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__sizeof__", + crate::gateway::make_builtin_function_with_arity_and_text_signature( + "__sizeof__", + list_descr_sizeof, + 1, + "($self, /)", + ), + ) + }; // listobject.py:2486 __class_getitem__ = interp2app( // generic_alias_class_getitem, as_classmethod=True) unsafe { diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs index 83c3c366b5a..52930c24519 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs @@ -460,11 +460,16 @@ pub(crate) fn fbw_store_journal_push( /// append's gate), so the rewind is allocation-free. // Consumed by the #171 `list.append` orthodox descent // (`try_walker_orthodox_list_append`). -pub(crate) fn fbw_list_journal_push_append(list: pyre_object::PyObjectRef, length_before: usize) { +pub(crate) fn fbw_list_journal_push_append( + list: pyre_object::PyObjectRef, + length_before: usize, + allocated_before: isize, +) { FBW_LIST_EFFECT_JOURNAL.with(|j| { j.borrow_mut().push(FbwListEffect::Append { list, length_before, + allocated_before, }) }); // gh#467: see `fbw_store_journal_push`. @@ -972,11 +977,12 @@ pub(crate) fn fbw_store_journal_rollback() { let mut entries = j.borrow_mut(); while let Some(entry) = entries.pop() { unsafe { - let (list, length_before) = match entry { + let (list, length_before, allocated_before) = match entry { FbwListEffect::Append { list, length_before, - } => (list, length_before), + allocated_before, + } => (list, length_before, allocated_before), FbwListEffect::PopEnd { list, length_before, @@ -1053,6 +1059,7 @@ pub(crate) fn fbw_store_journal_rollback() { // fold path records it); nothing to rewind. pyre_object::listobject::ListStrategy::Empty => {} } + pyre_object::listobject::w_list_set_allocated(list, allocated_before); } } }); diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs index 4b238c1326e..4ac9500e1ce 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs @@ -5966,6 +5966,11 @@ enum FbwListEffect { Append { list: pyre_object::PyObjectRef, length_before: usize, + /// `PyListObject.allocated` as the specialization read it before the + /// append. The length rewind alone would leave the over-allocation + /// the eager append computed, so the undo restores the field the same + /// way it restores the length. + allocated_before: isize, }, PopEnd { list: pyre_object::PyObjectRef, diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs index e86f997e97d..3d525c4a66e 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs @@ -2570,7 +2570,7 @@ pub(crate) fn try_execute_residual_call_via_executor( // augmented-assignment iteration at the trace-entry boundary. Let the // normal residual dispatch execute user special methods; its existing // user-frame effect accounting handles any later abort. - let inplace_list_journal: Option<(pyre_object::PyObjectRef, usize)> = + let inplace_list_journal: Option<(pyre_object::PyObjectRef, usize, isize)> = if call_descr.get_extra_info().pyre_helper == majit_ir::PyreHelperKind::BinaryOp && args.len() >= 3 && pyre_interpreter::runtime_ops::binary_op_tag_is_inplace(args[2]) @@ -2584,7 +2584,11 @@ pub(crate) fn try_execute_residual_call_via_executor( && pyre_object::pyobject::is_exact_list(rhs) && pyre_object::listobject::w_list_is_integer_strategy(rhs) { - Some((lhs, pyre_object::w_list_len(lhs))) + Some(( + lhs, + pyre_object::w_list_len(lhs), + pyre_object::listobject::w_list_allocated(lhs), + )) } else if pyre_object::pyobject::is_int_or_long(lhs) || pyre_object::pyobject::is_bool(lhs) || pyre_object::pyobject::is_float(lhs) @@ -2612,13 +2616,19 @@ pub(crate) fn try_execute_residual_call_via_executor( // rollback rewinds the one append and the deliver re-applies it exactly once // (the same `fbw_list_journal_push_append` contract the fold's own commit uses), // making the fall-through abort-safe instead of a silent double. - let list_append_journal: Option<(pyre_object::PyObjectRef, usize)> = + let list_append_journal: Option<(pyre_object::PyObjectRef, usize, isize)> = if call_descr.get_extra_info().pyre_helper == majit_ir::PyreHelperKind::ListAppendValue { let list = args .first() .map(|&a| a as usize as pyre_object::PyObjectRef); list.filter(|&l| !l.is_null() && unsafe { pyre_object::pyobject::is_list(l) }) - .map(|l| (l, unsafe { pyre_object::w_list_len(l) })) + .map(|l| unsafe { + ( + l, + pyre_object::w_list_len(l), + pyre_object::listobject::w_list_allocated(l), + ) + }) } else { None }; @@ -3460,9 +3470,9 @@ pub(crate) fn try_execute_residual_call_via_executor( // the deliver re-applies it exactly once. `result_i64 == lhs` // confirms the in-place mutation (list `__iadd__`/`__imul__` return // self) rather than a fresh-object op that merely shared the slot. - if let Some((lhs, len_before)) = inplace_list_journal { + if let Some((lhs, len_before, allocated_before)) = inplace_list_journal { if result_i64 as usize == lhs as usize { - fbw_list_journal_push_append(lhs, len_before); + fbw_list_journal_push_append(lhs, len_before, allocated_before); } } // A folded-decline `jit_list_append` fall-through (realloc-boundary @@ -3470,8 +3480,8 @@ pub(crate) fn try_execute_residual_call_via_executor( // the abort rollback rewinds it and the deliver re-applies exactly // once. The append always mutates its receiver (void `0` result), // so no in-place `result == lhs` re-check is needed. - if let Some((list, len_before)) = list_append_journal { - fbw_list_journal_push_append(list, len_before); + if let Some((list, len_before, allocated_before)) = list_append_journal { + fbw_list_journal_push_append(list, len_before, allocated_before); } // pyjitpl.py `result_box.value = result` analogue — stamp // the recorded OpRef with the executed concrete so downstream diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs index 7b280d2a41b..dde83b53758 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs @@ -9414,6 +9414,7 @@ pub(crate) fn orthodox_list_append_commit( value: pyre_object::PyObjectRef, len_before: usize, ) -> Result<(), DispatchError> { + let allocated_before = unsafe { pyre_object::listobject::w_list_allocated(inner_self) }; // `w_list_append` unboxes its `value` inside an inline sub-walk. A // virtual range item must be materialized at that call boundary: otherwise // the sub-walk's snapshot exports its raw payload as a loop-carried scalar, @@ -9717,7 +9718,7 @@ pub(crate) fn orthodox_list_append_commit( // iterations, a traceback name list with its last frame doubled). // Re-read the length instead of assuming which side ran: it is the // receiver's own state, so it answers for both. - fbw_list_journal_push_append(inner_self, len_before); + fbw_list_journal_push_append(inner_self, len_before, allocated_before); if unsafe { pyre_object::w_list_len(inner_self) } == len_before { unsafe { pyre_object::w_list_append(inner_self, value) }; } diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs index 2f948607ff2..96414549b6d 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs @@ -2298,6 +2298,7 @@ fn append_journal_rollback_rewinds_length() { // (the only shape the arm specializes). unsafe { w_list_append(list, w_int_new(40)) }; let len_before = unsafe { w_list_len(list) }; + let allocated_before = unsafe { pyre_object::listobject::w_list_allocated(list) }; assert_eq!(len_before, 4); assert!( unsafe { w_list_can_append_without_realloc(list) }, @@ -2307,7 +2308,7 @@ fn append_journal_rollback_rewinds_length() { // Rollback path: journal push + eager append (production order, see // try_walker_orthodox_list_append), then a non-commit exit rewinds // the length. - super::fbw_list_journal_push_append(list, len_before); + super::fbw_list_journal_push_append(list, len_before, allocated_before); unsafe { w_list_append(list, w_int_new(50)) }; assert_eq!(unsafe { w_list_len(list) }, 5); super::fbw_store_journal_rollback(); @@ -2316,9 +2317,14 @@ fn append_journal_rollback_rewinds_length() { len_before, "non-commit walk must rewind the eager append's length" ); + assert_eq!( + unsafe { pyre_object::listobject::w_list_allocated(list) }, + allocated_before, + "non-commit walk must restore the logical allocation" + ); // Commit path: the eager append stands; the log is dropped. - super::fbw_list_journal_push_append(list, len_before); + super::fbw_list_journal_push_append(list, len_before, allocated_before); unsafe { w_list_append(list, w_int_new(60)) }; super::fbw_store_journal_commit(); assert_eq!( @@ -2438,13 +2444,14 @@ fn append_journal_rollback_rewinds_object_length() { // (the only shape the journal records). unsafe { w_list_append(list, w_none()) }; let len_before = unsafe { w_list_len(list) }; + let allocated_before = unsafe { pyre_object::listobject::w_list_allocated(list) }; assert_eq!(len_before, 4); assert!( unsafe { w_list_can_append_without_realloc(list) }, "post-grow object list must have spare capacity for the in-place append" ); - super::fbw_list_journal_push_append(list, len_before); + super::fbw_list_journal_push_append(list, len_before, allocated_before); unsafe { w_list_append(list, w_none()) }; assert_eq!(unsafe { w_list_len(list) }, 5); super::fbw_store_journal_rollback(); diff --git a/pyre/pyre-object/src/float_array.rs b/pyre/pyre-object/src/float_array.rs index d442c0e2a4c..cc80b3f6a50 100644 --- a/pyre/pyre-object/src/float_array.rs +++ b/pyre/pyre-object/src/float_array.rs @@ -21,7 +21,7 @@ pub struct FloatArray { /// `Ptr(GcArray(Float))` — the backing block (`l.items`). Always non-null. pub block: *mut TypedItemsBlock, /// Live length (rlist.py:116 `("length", Signed)`). - len: usize, + pub(crate) len: usize, } pub const FLOAT_ARRAY_BLOCK_OFFSET: usize = std::mem::offset_of!(FloatArray, block); diff --git a/pyre/pyre-object/src/int_array.rs b/pyre/pyre-object/src/int_array.rs index 5aa0f57d766..140efdfc5a1 100644 --- a/pyre/pyre-object/src/int_array.rs +++ b/pyre/pyre-object/src/int_array.rs @@ -25,7 +25,7 @@ pub struct IntArray { /// `Ptr(GcArray(Signed))` — the backing block (`l.items`). Always non-null. pub block: *mut TypedItemsBlock, /// Live length (rlist.py:116 `("length", Signed)`). - len: usize, + pub(crate) len: usize, } pub const INT_ARRAY_BLOCK_OFFSET: usize = std::mem::offset_of!(IntArray, block); diff --git a/pyre/pyre-object/src/listobject.rs b/pyre/pyre-object/src/listobject.rs index 9050006bf4a..7015296fda3 100644 --- a/pyre/pyre-object/src/listobject.rs +++ b/pyre/pyre-object/src/listobject.rs @@ -104,6 +104,10 @@ pub enum ListStrategy { #[repr(C)] pub struct W_ListObject { pub ob_header: PyObject, + /// CPython 3.14 `PyListObject.allocated`. The strategy backings use + /// RPython arrays whose physical growth policy differs, while + /// `list.__sizeof__()` exposes this logical pointer-slot allocation. + pub allocated: isize, /// Live length under the Object strategy. Upstream `l.length` /// (rlist.py:116). Under Integer/Float strategies this mirrors /// `int_items.len()` / `float_items.len()` only when a strategy @@ -138,6 +142,43 @@ pub const W_LIST_GC_TYPE_ID: u32 = 7; pub const W_LIST_OBJECT_SIZE: usize = std::mem::size_of::(); impl W_ListObject { + #[inline] + fn live_len(&self) -> usize { + match self.strategy { + ListStrategy::Empty => 0, + ListStrategy::Object => self.length, + // Direct rlist `length` field reads keep this helper in the + // annotator's structural subset; the public `.len()` wrappers are + // host collection conveniences that translate as `__len__`. + ListStrategy::Integer => self.int_items.len, + ListStrategy::Float => self.float_items.len, + } + } + + /// CPython 3.14 `list_resize`'s `allocated` calculation. + fn resized_allocation(&self, old_size: usize, new_size: usize) -> usize { + let allocated = if self.allocated < 0 { + 0 + } else { + self.allocated as usize + }; + if allocated >= new_size && new_size >= (allocated >> 1) { + return allocated; + } + let mut new_allocated = (new_size + (new_size >> 3) + 6) & !3; + if new_size > old_size && new_size - old_size > new_allocated.saturating_sub(new_size) { + new_allocated = (new_size + 3) & !3; + } + if new_size == 0 { + new_allocated = 0; + } + new_allocated + } + + fn sync_allocated(&mut self, old_size: usize) { + self.allocated = self.resized_allocation(old_size, self.live_len()) as isize; + } + /// Borrow a slice over object-strategy items. Must only be called /// when `self.strategy == ListStrategy::Object`. #[inline] @@ -821,6 +862,7 @@ fn w_list_new_with_strategy(items: Vec, strategy: ListStrategy) -> if raw.is_null() { let boxed = Box::new(W_ListObject { ob_header: header, + allocated: items.len() as isize, length, items: items_block, strategy, @@ -839,6 +881,7 @@ fn w_list_new_with_strategy(items: Vec, strategy: ListStrategy) -> raw as *mut W_ListObject, W_ListObject { ob_header: header, + allocated: items.len() as isize, length, items: items_block, strategy, @@ -1182,7 +1225,33 @@ pub unsafe fn w_list_append(obj: PyObjectRef, value: PyObjectRef) { let _list_guard = w_list_lock(obj); let obj = crate::gc_roots::shadow_stack_get(root_base); let value = crate::gc_roots::shadow_stack_get(root_base + 1); - w_list_append_inner(obj, value) + let list = &mut *(obj as *mut W_ListObject); + let old_size = list.live_len(); + w_list_append_inner(obj, value); + list.sync_allocated(old_size); +} + +/// CPython `list_extend_iter_lock_held`'s direct-store append while its +/// length-hint reservation still has a free logical slot. The physical +/// strategy append is identical; only `list_resize` is skipped. +pub unsafe fn w_list_append_preallocated(obj: PyObjectRef, value: PyObjectRef) { + let _roots = crate::gc_roots::push_roots(); + let root_base = crate::gc_roots::shadow_stack_len(); + crate::gc_roots::pin_root(obj); + crate::gc_roots::pin_root(value); + let obj = crate::gc_roots::shadow_stack_get(root_base); + let _list_guard = w_list_lock(obj); + let obj = crate::gc_roots::shadow_stack_get(root_base); + let value = crate::gc_roots::shadow_stack_get(root_base + 1); + let list = &mut *(obj as *mut W_ListObject); + let old_size = list.live_len(); + let old_allocated = list.allocated; + w_list_append_inner(obj, value); + if old_allocated > old_size as isize { + list.allocated = old_allocated; + } else { + list.sync_allocated(old_size); + } } /// [`w_list_append`]'s body, run with the list's guard already held. @@ -1338,6 +1407,74 @@ pub unsafe fn w_list_len(obj: PyObjectRef) -> usize { } } +/// CPython-visible `PyListObject.allocated` under the list's mutation lock. +pub unsafe fn w_list_allocated(obj: PyObjectRef) -> isize { + let _roots = crate::gc_roots::push_roots(); + let root_base = crate::gc_roots::shadow_stack_len(); + crate::gc_roots::pin_root(obj); + let obj = crate::gc_roots::shadow_stack_get(root_base); + let _list_guard = w_list_lock(obj); + let obj = crate::gc_roots::shadow_stack_get(root_base); + (*(obj as *const W_ListObject)).allocated +} + +/// Reserve CPython's logical slots before `list.extend` consumes its source. +pub unsafe fn w_list_reserve_for_extend(obj: PyObjectRef, extra: usize) { + if extra == 0 { + return; + } + let _list_guard = w_list_lock(obj); + let list = &mut *(obj as *mut W_ListObject); + let old_size = list.live_len(); + if list.allocated == 0 { + list.allocated = ((extra + 1) & !1) as isize; + } else if let Some(new_size) = old_size.checked_add(extra) { + list.allocated = list.resized_allocation(old_size, new_size) as isize; + } +} + +/// `list_extend_set` / `list_extend_dict` use ordinary `list_resize` even +/// when the destination has no backing array; unlike sequence-fast extension +/// they do not call `list_preallocate_exact`. +pub unsafe fn w_list_resize_for_extend(obj: PyObjectRef, extra: usize) { + if extra == 0 { + return; + } + let _list_guard = w_list_lock(obj); + let list = &mut *(obj as *mut W_ListObject); + let old_size = list.live_len(); + if let Some(new_size) = old_size.checked_add(extra) { + list.allocated = list.resized_allocation(old_size, new_size) as isize; + } +} + +/// `list_extend_iter_lock_held` trims an overestimated length hint after the +/// iterator ends, using ordinary `list_resize` shrink rules. +pub unsafe fn w_list_finish_extend(obj: PyObjectRef) { + let _list_guard = w_list_lock(obj); + let list = &mut *(obj as *mut W_ListObject); + let size = list.live_len(); + if list.allocated > size as isize { + list.allocated = list.resized_allocation(size, size) as isize; + } +} + +/// Recompute one CPython `list_resize` after a pyre implementation performed +/// a batch mutation as several primitive removals. +pub unsafe fn w_list_finish_batch_resize(obj: PyObjectRef, old_size: usize, old_allocated: isize) { + let _list_guard = w_list_lock(obj); + let list = &mut *(obj as *mut W_ListObject); + list.allocated = old_allocated; + list.sync_allocated(old_size); +} + +/// Set CPython's raw `PyListObject.allocated` field. `list.sort` uses `-1` +/// while the saved item array is detached, then restores the previous value. +pub unsafe fn w_list_set_allocated(obj: PyObjectRef, allocated: isize) { + let _list_guard = w_list_lock(obj); + (*(obj as *mut W_ListObject)).allocated = allocated; +} + /// Whether `obj` is a list currently backed by the Integer strategy — the /// only shape [`w_list_int_set_len`] can rewind. /// @@ -1508,6 +1645,7 @@ fn normalize_insert_index(index: i64, len: usize) -> usize { /// switches to Object only when incompatible. pub unsafe fn w_list_insert(obj: PyObjectRef, index: i64, value: PyObjectRef) { let list = &mut *(obj as *mut W_ListObject); + let old_size = list.live_len(); match list.strategy { // EmptyListStrategy doesn't override insert, so it falls through // ListStrategy.insert (listobject.py:983) → switches to typed strategy @@ -1515,11 +1653,13 @@ pub unsafe fn w_list_insert(obj: PyObjectRef, index: i64, value: PyObjectRef) { ListStrategy::Empty => { switch_to_correct_strategy(list, value); w_list_insert(obj, index, value); + return; } ListStrategy::Integer => { if is_plain_int1(value) { let idx = normalize_insert_index(index, list.int_items.len()); list.int_items.insert(idx, plain_int_w(value)); + list.sync_allocated(old_size); return; } switch_to_object_strategy(list); @@ -1529,6 +1669,7 @@ pub unsafe fn w_list_insert(obj: PyObjectRef, index: i64, value: PyObjectRef) { if is_float_strategy_item(value) { let idx = normalize_insert_index(index, list.float_items.len()); list.float_items.insert(idx, w_float_get_value(value)); + list.sync_allocated(old_size); return; } switch_to_object_strategy(list); @@ -1538,6 +1679,7 @@ pub unsafe fn w_list_insert(obj: PyObjectRef, index: i64, value: PyObjectRef) { let idx = normalize_insert_index(index, list.length); list.object_insert(idx, value); list_write_barrier(obj); + list.sync_allocated(old_size); } } } @@ -1552,7 +1694,8 @@ pub unsafe fn w_list_pop(obj: PyObjectRef, index: i64) -> Option { let _list_guard = w_list_lock(obj); let obj = crate::gc_roots::shadow_stack_get(root_base); let list = &mut *(obj as *mut W_ListObject); - match list.strategy { + let old_size = list.live_len(); + let result = match list.strategy { // listobject.py:1180 EmptyListStrategy.pop raises IndexError. ListStrategy::Empty => None, ListStrategy::Integer => { @@ -1590,7 +1733,11 @@ pub unsafe fn w_list_pop(obj: PyObjectRef, index: i64) -> Option { } Some(list.object_remove(idx as usize)) } + }; + if result.is_some() { + list.sync_allocated(old_size); } + result } /// Remove and return the last item. Returns `None` if empty. @@ -1616,7 +1763,7 @@ pub unsafe fn w_list_pop_end(obj: PyObjectRef) -> Option { let obj = crate::gc_roots::shadow_stack_get(root_base); let _list_guard = w_list_lock(obj); let obj = crate::gc_roots::shadow_stack_get(root_base); - let list = &*(obj as *const W_ListObject); + let list = &mut *(obj as *mut W_ListObject); let length = match list.strategy { ListStrategy::Empty => 0, ListStrategy::Integer => ll_list_int_length(list), @@ -1626,7 +1773,12 @@ pub unsafe fn w_list_pop_end(obj: PyObjectRef) -> Option { if length == 0 { None } else { - Some(w_list_pop_end_inner(obj)) + // `length` is the pre-pop `live_len`, so it is the `old_size` + // `list_resize` shrinks from. The metadata stays out here, as it does + // for `w_list_append`, because the inner body is descended by the fold. + let w_item = w_list_pop_end_inner(obj); + list.sync_allocated(length); + Some(w_item) } } @@ -1771,10 +1923,17 @@ pub unsafe fn w_list_clear(obj: PyObjectRef) { let _list_guard = w_list_lock(obj); let obj = crate::gc_roots::shadow_stack_get(root_base); let list = &mut *(obj as *mut W_ListObject); + // `list_sort_impl` has already detached the storage and set size to zero. + // CPython's `list.clear()` is then a no-op and leaves the -1 sentinel, so + // it must not spuriously report "list modified during sort". + if list.live_len() == 0 && list.allocated == -1 { + return; + } list.drop_object_items(); list.int_items.install(IntArray::from_vec(Vec::new())); list.float_items.install(FloatArray::from_vec(Vec::new())); list.strategy = ListStrategy::Empty; + list.allocated = 0; } /// listobject.py:1154-1168 EmptyListStrategy.switch_to_correct_strategy — @@ -1808,6 +1967,8 @@ pub unsafe fn w_list_reverse(obj: PyObjectRef) { /// Strategy-preserving: drains from typed storage. pub unsafe fn w_list_delslice(obj: PyObjectRef, start: usize, end: usize) { let list = &mut *(obj as *mut W_ListObject); + let old_size = list.live_len(); + let mut changed = false; match list.strategy { // listobject.py:1177 EmptyListStrategy.deleteslice is a no-op (pass). ListStrategy::Empty => {} @@ -1817,6 +1978,7 @@ pub unsafe fn w_list_delslice(obj: PyObjectRef, start: usize, end: usize) { let e = end.min(len); if s < e { list.int_items.drain(s..e); + changed = true; } } ListStrategy::Float => { @@ -1825,6 +1987,7 @@ pub unsafe fn w_list_delslice(obj: PyObjectRef, start: usize, end: usize) { let e = end.min(len); if s < e { list.float_items.drain(s..e); + changed = true; } } ListStrategy::Object => { @@ -1833,9 +1996,13 @@ pub unsafe fn w_list_delslice(obj: PyObjectRef, start: usize, end: usize) { let e = end.min(len); if s < e { list.object_drain(s..e); + changed = true; } } } + if changed { + list.sync_allocated(old_size); + } } /// listobject.py:1613-1631 IntegerListStrategy._safe_find_or_count @@ -1978,6 +2145,23 @@ pub unsafe fn w_list_setslice( start: usize, end: usize, w_other: PyObjectRef, +) -> Result<(), &'static str> { + let old_size = (&*(obj as *const W_ListObject)).live_len(); + let result = w_list_setslice_inner(obj, start, end, w_other); + if result.is_ok() { + let list = &mut *(obj as *mut W_ListObject); + if list.live_len() != old_size { + list.sync_allocated(old_size); + } + } + result +} + +unsafe fn w_list_setslice_inner( + obj: PyObjectRef, + start: usize, + end: usize, + w_other: PyObjectRef, ) -> Result<(), &'static str> { let _roots = crate::gc_roots::push_roots(); let root_base = crate::gc_roots::shadow_stack_len(); From a97bdebfd83720c91778ad46e51d6abf5ff39027 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 03:38:06 +0900 Subject: [PATCH 10/26] jit: reject null callable resume images --- .../src/jitcode_dispatch/inline_call.rs | 9 ++++- .../src/jitcode_dispatch/resume_snapshot.rs | 34 ++++++++++++++----- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs index d59cb290847..85016e3f962 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs @@ -1311,6 +1311,13 @@ pub(crate) fn reconstructed_all_ref_call_stack( if fresh.is_empty() { return None; } + // Validate the CALL operand slice itself, not the complete reconstructed + // stack. A CALL inside WITH/FOR_ITER can retain non-null prefix operands; + // checking `stack.first()` after prepending them lets an unresolved NULL + // callable slip through and publishes an invalid frame at the CALL. + if !matches!(fresh.first(), Some(ConcreteValue::Ref(r)) if !r.is_null()) { + return None; + } // The encoded residual args describe only the CALL operands. Values can // remain below them on the Python operand stack (notably the iterator of // an enclosing FOR_ITER). RPython resumes the complete MIFrame stack, so @@ -1333,7 +1340,7 @@ pub(crate) fn reconstructed_all_ref_call_stack( _ => return None, } } - stack.first().is_some_and(|c| !c.is_null()).then_some(stack) + Some(stack) } /// Fold a keyword call's `kwnames`->parameter permutation at trace time so a diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs index 60fe4fd2d93..7c3f5fcfe37 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs @@ -1275,14 +1275,14 @@ pub(crate) fn collect_call_stack_overrides( caller_sym: &Sym, ctx: &WalkContext<'_, '_, Sym>, call_jitcode_pc: usize, -) -> Vec<(usize, pyre_object::PyObjectRef)> { +) -> Option> { if caller_sym.jitcode().is_null() { - return Vec::new(); + return None; } let (nlocals, depth, pcdep_entries) = unsafe { let jc = &*caller_sym.jitcode(); if jc.payload.code_ptr.is_null() { - return Vec::new(); + return None; } // The caller CALL key is a plain JitCode→Python inversion, so use the // certified predecessor twins rather than the marker/trivia flavor @@ -1299,7 +1299,7 @@ pub(crate) fn collect_call_stack_overrides( }; let stack_end = nlocals + depth; if depth == 0 { - return Vec::new(); + return Some(Vec::new()); } let mut overrides = Vec::new(); if ctx.vstack_valid && ctx.vstack_depth == depth && ctx.vstack_boxes.len() >= depth { @@ -1375,13 +1375,28 @@ pub(crate) fn collect_call_stack_overrides( // guessing: `[callable, null_or_self, arg0 .. arg_{argc-1}]` ends at // `stack_end`, so the sentinel sits `argc + 1` below it, right under the // arguments and right above the callable. - if let Some(slot) = call_null_or_self_slot(caller_sym, call_jitcode_pc, stack_end) - && slot >= nlocals - && !overrides.iter().any(|&(present, _)| present == slot) + let null_or_self_slot = call_null_or_self_slot(caller_sym, call_jitcode_pc, stack_end)?; + if null_or_self_slot >= nlocals + && !overrides + .iter() + .any(|&(present, _)| present == null_or_self_slot) { - overrides.push((slot, std::ptr::null_mut::() as pyre_object::PyObjectRef)); + overrides.push(( + null_or_self_slot, + std::ptr::null_mut::() as pyre_object::PyObjectRef, + )); } + // The slot immediately below null_or_self is the callable. Ref(0) is a + // valid value only in the null_or_self slot above; in the callable slot it + // means the sparse vstack/color reconstruction is incomplete. Decline + // the parent-frame image instead of publishing a CALL that will dispatch + // through a null object. + let callable_slot = null_or_self_slot.checked_sub(1)?; overrides + .iter() + .find_map(|&(slot, value)| (slot == callable_slot).then_some(value)) + .filter(|value| !value.is_null())?; + Some(overrides) } /// Absolute frame slot holding the `null_or_self` operand of the `CALL` whose @@ -1735,7 +1750,8 @@ pub(crate) fn compute_inline_caller_frame( if depth == 0 && !call_is_void { return Err(unavail("Unavail::Top/DepthZero")); } - let call_stack_overrides = collect_call_stack_overrides(caller_sym, ctx, call_jit_pc); + let call_stack_overrides = collect_call_stack_overrides(caller_sym, ctx, call_jit_pc) + .ok_or_else(|| unavail("Unavail::Top/CallStack"))?; // #73: the result slot's color comes from the codewriter-precomputed // `result_color_at_pc` (top-of-stack color at the return pc), not the flat // `stack_slot_color_map` — when the result remains live, it is not a live From ee1785de97addea4d142047548112ac8a84fb74c Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 04:01:08 +0900 Subject: [PATCH 11/26] weakref: expose CPython layout metadata --- .../src/module/_weakref/interp__weakref.rs | 18 +++++++++++++++--- pyre/pyre-interpreter/src/typedef.rs | 19 +++++++++++++++++++ pyre/pyre-object/src/weakref.rs | 8 ++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs b/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs index f086949852a..b6941ef4f41 100644 --- a/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs +++ b/pyre/pyre-interpreter/src/module/_weakref/interp__weakref.rs @@ -223,7 +223,12 @@ pub fn weakref_type() -> PyObjectRef { // CPython exposes `_weakref.ref` as `weakref.ReferenceType`. // The dotted builtin name supplies the public module while the type // metadata getters expose the final component as the bare name. - let tp = crate::typedef::make_builtin_type("weakref.ReferenceType", init_weakref_type); + let tp = crate::typedef::make_builtin_type_with_layout( + "weakref.ReferenceType", + init_weakref_type, + crate::typedef::w_object(), + &pyre_object::weakref::WEAKREF_LAYOUT_TYPE as *const PyType, + ); unsafe { pyre_object::w_type_set_hasdict(tp, true) }; tp as usize }) as PyObjectRef @@ -271,7 +276,12 @@ fn init_proxy_type(ns: PyObjectRef) { #[majit_macros::dont_look_inside] pub fn proxy_type() -> PyObjectRef { *PROXY_TYPE.get_or_init(|| { - let tp = crate::typedef::make_builtin_type("weakref.ProxyType", init_proxy_type); + let tp = crate::typedef::make_builtin_type_with_layout( + "weakref.ProxyType", + init_proxy_type, + crate::typedef::w_object(), + &pyre_object::weakref::WEAKREF_LAYOUT_TYPE as *const PyType, + ); unsafe { pyre_object::w_type_set_hasdict(tp, true); pyre_object::w_type_set_acceptable_as_base_class(tp, false); @@ -330,9 +340,11 @@ fn init_callable_proxy_type(ns: PyObjectRef) { #[majit_macros::dont_look_inside] pub fn callable_proxy_type() -> PyObjectRef { *CALLABLE_PROXY_TYPE.get_or_init(|| { - let tp = crate::typedef::make_builtin_type( + let tp = crate::typedef::make_builtin_type_with_layout( "weakref.CallableProxyType", init_callable_proxy_type, + crate::typedef::w_object(), + &pyre_object::weakref::WEAKREF_LAYOUT_TYPE as *const PyType, ); unsafe { pyre_object::w_type_set_hasdict(tp, true); diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index eb2895530cf..31e311589d6 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -10334,6 +10334,12 @@ fn cpython_type_layout(w_type: PyObjectRef) -> Option<(i64, i64)> { (6 * word, 0) } else if is(&pyre_object::functional::ENUMERATE_TYPE) { (7 * word, 0) + } else if is(&pyre_object::weakref::WEAKREF_LAYOUT_TYPE) { + // CPython 3.14 `PyWeakReference`: object header, doubly-linked + // weakref list, callback, hash/cache word and vectorcall slot. + // PyPy likewise gives W_WeakrefBase/W_Weakref their own typedef; + // subclasses append their declared slots to this prefix. + (8 * word, 0) } else { return None; }; @@ -29113,6 +29119,19 @@ mod tests { assert!(crate::type_dict_contains(object_type, "__class__")); } + #[test] + fn weakref_types_have_cpython_314_layout_metadata() { + crate::typedef::init_typeobjects(); + let word = std::mem::size_of::() as i64; + for w_type in [ + crate::module::_weakref::interp__weakref::weakref_type(), + crate::module::_weakref::interp__weakref::proxy_type(), + crate::module::_weakref::interp__weakref::callable_proxy_type(), + ] { + assert_eq!(super::cpython_type_layout(w_type), Some((8 * word, 0))); + } + } + #[test] fn test_ellipsis_has_registered_typeobject() { crate::typedef::init_typeobjects(); diff --git a/pyre/pyre-object/src/weakref.rs b/pyre/pyre-object/src/weakref.rs index 02219c85023..5b22210766f 100644 --- a/pyre/pyre-object/src/weakref.rs +++ b/pyre/pyre-object/src/weakref.rs @@ -12,6 +12,14 @@ use crate::gc_hook::try_gc_alloc; use crate::pyobject::*; +/// Interpreter-level layout tag shared by PyPy's `W_Weakref` and +/// `W_AbstractProxy` families. The current host representation still uses +/// `W_ObjectObject` storage, but the type's `Layout.typedef` must remain +/// distinct from plain `object`: user subclasses inherit the weak-reference +/// prefix before their own slots, and CPython 3.14 exposes that prefix through +/// `type.__basicsize__`. +pub static WEAKREF_LAYOUT_TYPE: PyType = new_pytype("W_WeakrefBase"); + /// GC type id for the WEAKREF GcStruct. Registered by /// `pyre-jit::eval::init` after `W_INT_MUTABLE_CELL` and before the /// per-exception kind loop. A `debug_assert_eq!` in the registration From c5ee4a919f03a39627fd9e176aca577afed4cf35 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 04:24:37 +0900 Subject: [PATCH 12/26] jit: route guard exceptions to trailing catches --- majit/majit-metainterp/src/blackhole.rs | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/majit/majit-metainterp/src/blackhole.rs b/majit/majit-metainterp/src/blackhole.rs index bb9a4e9e879..3d3cd77dc9b 100644 --- a/majit/majit-metainterp/src/blackhole.rs +++ b/majit/majit-metainterp/src/blackhole.rs @@ -1213,6 +1213,17 @@ impl BlackholeInterpreter { if opcode == self.op_catch_exception { return self.route_to_catch(position, exc_value); } + // A guard resume coordinate may name the successor block's entry + // `-live-`, before that block mirrors the virtualizable and reaches + // the raising operation's trailing `-live-`. The flattener emits the + // `catch_exception` immediately after that trailing marker + // (`flatten.py:206-217`). Walk forward to the first such marker and + // accept only its immediately-following catch; crossing any other + // operation after it means this exception belongs to no handler at + // the resumed call site. + if let Some(catch_pos) = self.find_catch_after_resume_live(resume_live_pos) { + return self.route_to_catch(catch_pos, exc_value); + } // Backward case (after-residual-call guard): pyre resumes the post-call // `GUARD_NO_EXCEPTION` at the next opcode's `-live-` // (`pc_map[fallthrough_pc]`, jitcode_dispatch.rs / capture_resumedata), @@ -1365,6 +1376,31 @@ impl BlackholeInterpreter { None } + fn find_catch_after_resume_live(&self, resume_live_pos: usize) -> Option { + let code = &self.jitcode.code; + let startpoints = self.jitcode.startpoints.as_ref()?; + let mut points: Vec = startpoints + .iter() + .copied() + .filter(|&q| q > resume_live_pos) + .collect(); + points.sort_unstable(); + let mut crossed_trailing_live = false; + for q in points { + let op = code[q]; + if op == self.op_catch_exception { + return crossed_trailing_live.then_some(q); + } + if crossed_trailing_live { + return None; + } + if op == self.op_live { + crossed_trailing_live = true; + } + } + None + } + /// blackhole.py:424-439 handle_rvmprof_enter. pub fn handle_rvmprof_enter(&mut self) { let code = &self.jitcode.code; @@ -4284,6 +4320,35 @@ mod tests { assert_eq!(bh.return_type, BhReturnType::Int); } + #[test] + fn test_guard_exception_resume_finds_catch_after_successor_sync() { + let mut asm = majit_translate::codewriter::assembler::Assembler::new(); + let mut b = JitCodeBuilder::default(); + let resume_pc = b.current_pos(); + b.live(&mut asm, &[], &[], &[]); + // The normal-flow successor mirrors virtualizable state before + // the can-raise block's trailing live/catch pair. + b.load_const_i_value(0, 1); + b.load_const_i_value(1, 2); + b.live(&mut asm, &[], &[], &[]); + let handler_lbl = b.new_label(); + b.catch_exception(handler_lbl); + b.load_const_i_value(2, 99); + b.int_return(2); + b.mark_label(handler_lbl); + let handler_pc = b.current_pos(); + b.load_const_i_value(2, 42); + b.int_return(2); + let jitcode = b.finish(); + + let mut builder = super::build_inline_call_only_bh_builder(); + let mut bh = builder.acquire_interp(); + bh.setposition(std::sync::Arc::new(jitcode), resume_pc); + + assert!(bh.handle_exception_in_frame(0xCAFE_F00D)); + assert_eq!(bh.position, handler_pc); + } + thread_local! { /// Address of the interpreter's `exception_last_value` slot, read /// back by [`probe_exception_slot_at_record_time`]. From a68fb4b668e49a246b8182f46cc43c6c5b49de78 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 04:37:43 +0900 Subject: [PATCH 13/26] generator: clear fresh frames through gi_frame --- pyre/pyre-interpreter/src/baseobjspace.rs | 5 ++++- pyre/pyre-interpreter/src/pyframe.rs | 11 +++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 10a6bd6cf1a..3277c493c32 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -16886,7 +16886,10 @@ pub(crate) fn property_descr_delete_impl(args: &[PyObjectRef]) -> PyResult { /// generator.py:313-315 `frame_is_finished` plus Python 3.14's eager frame /// clearing on `close()`. Dropping the generator's frame edge releases all /// suspended locals; an escaped `gi_frame` remains a valid, cleared frame. -unsafe fn generator_frame_is_finished(gen_obj: PyObjectRef, frame: &mut crate::pyframe::PyFrame) { +pub(crate) unsafe fn generator_frame_is_finished( + gen_obj: PyObjectRef, + frame: &mut crate::pyframe::PyFrame, +) { use pyre_object::generator::*; unsafe { w_generator_set_exhausted(gen_obj) }; frame.set_frame_finished_execution(true); diff --git a/pyre/pyre-interpreter/src/pyframe.rs b/pyre/pyre-interpreter/src/pyframe.rs index 293bdbc24d7..d4f089e6442 100644 --- a/pyre/pyre-interpreter/src/pyframe.rs +++ b/pyre/pyre-interpreter/src/pyframe.rs @@ -3292,6 +3292,17 @@ impl PyFrame { )); } crate::baseobjspace::generator_finalize(w_gen)?; + // CPython 3.14 `frame_clear_impl` finishes a never-started + // generator after `_PyGen_Finalize`, then clears the owned + // interpreter frame. The suspended case was rejected above, + // so this is the same completion path used by close() and + // normal generator return, including severing `gi_frame`. + unsafe { crate::baseobjspace::generator_frame_is_finished(w_gen, self) }; + let ec = crate::call::getexecutioncontext() + as *mut crate::executioncontext::ExecutionContext; + if !ec.is_null() { + unsafe { (*ec).finalize_explicitly_cleared_frame_references() }; + } return Ok(()); } // pyframe.py:815-820: a dead `f_generator_wref` simply skips the From 255997387c46fe8c952da0af3bbd5dfbcbd4de81 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 04:51:37 +0900 Subject: [PATCH 14/26] generator: release consumed close exceptions --- pyre/pyre-interpreter/src/baseobjspace.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 3277c493c32..f114a94653c 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -17566,9 +17566,20 @@ pub(crate) fn generator_close_method(args: &[PyObjectRef]) -> PyResult { let w_exc = e.to_exc_object(); let _roots = pyre_object::gc_roots::push_roots(); pyre_object::gc_roots::pin_root(w_exc); - getattr_str(w_exc, "value").or_else(|_| Ok(w_none())) + let value = getattr_str(w_exc, "value").or_else(|_| Ok(w_none())); + // The StopIteration has been consumed at this Rust-level catch; + // unlike a Python handler, no PUSH_EXC_INFO will clear the + // temporary propagation root for us. + crate::eval::set_in_flight_exception(PY_NULL); + value + } + Err(e) if e.kind == PyErrorKind::GeneratorExit => { + // generator.py:265 `except OperationError as e` consumes the + // GeneratorExit after matching it. Mirror PUSH_EXC_INFO's + // ownership transfer by ending pyre's propagation root here. + crate::eval::set_in_flight_exception(PY_NULL); + Ok(w_none()) } - Err(e) if e.kind == PyErrorKind::GeneratorExit => Ok(w_none()), Err(e) => Err(e), }; unsafe { From fbb793b47dfa76decec0a4c722343935c1b2f22d Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 05:46:36 +0900 Subject: [PATCH 15/26] jit: preserve generic sequence iterator exhaustion --- extra_tests/test_itertools.py | 15 +++++++++++++++ .../src/jitcode_dispatch/residual_call.rs | 9 ++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/extra_tests/test_itertools.py b/extra_tests/test_itertools.py index d75e0e8343d..8fba5f44415 100644 --- a/extra_tests/test_itertools.py +++ b/extra_tests/test_itertools.py @@ -13,3 +13,18 @@ def test_islice_maxint(): def test_islice_largeint(): slic = itertools.islice(itertools.count(), 1, 10, sys.maxsize - 20) assert len(list(slic)) == 1 + + +def test_generic_sequence_iterator_exhaustion_after_jit(): + class Sequence: + def __init__(self, values): + self.values = values + + def __getitem__(self, index): + return self.values[index] + + total = 0 + for _ in range(20): + for value in Sequence(range(1000)): + total += value + assert total == 9_990_000 diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs index 3d525c4a66e..4d2f0d94767 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs @@ -5016,6 +5016,10 @@ pub(crate) fn dispatch_residual_call_iRd_kind( // Range FOR_ITER is a C-level iterator advance. Re-emit its field // updates so the opaque ForIterNext residual cannot invalidate optheap; // other iterator families retain the residual and its Python semantics. + // In particular, a generic W_SeqIterObject must stay residual: its PyPy + // `W_SeqIterObject.descr_next` frame catches IndexError from `__getitem__` + // and turns it into iterator exhaustion. Inlining only `__getitem__` + // drops that catch frame, so GUARD_NO_EXCEPTION leaks IndexError on deopt. // The specialization supplies the same Ref result that the residual would, // including NULL for exhaustion, so the codewriter's trailing // GuardNonnull remains the only loop-exit guard. @@ -5026,11 +5030,6 @@ pub(crate) fn dispatch_residual_call_iRd_kind( write_residual_call_result_to_dst(ctx, op.pc, dst, dst_bank, item_op)?; return Ok((DispatchOutcome::Continue, op.next_pc)); } - if let Some(outcome) = try_walker_specialize_seqiter_getitem_next( - ctx, op, code, funcptr, &r_args, call_descr, dst, dst_bank, - )? { - return Ok(outcome); - } } // Emit MAKE_FUNCTION's `Function.__init__` as New + SetField so a `def` in a From 74d627672b207112b5d0f864a7b0093a1aec77ed Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 06:01:02 +0900 Subject: [PATCH 16/26] io: make opened descriptors non-inheritable --- .../parity_tests/open_non_inheritable.py | 27 +++++++++++++++++ pyre/pyre-interpreter/src/builtins.rs | 30 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 pyre/extra_tests/parity_tests/open_non_inheritable.py diff --git a/pyre/extra_tests/parity_tests/open_non_inheritable.py b/pyre/extra_tests/parity_tests/open_non_inheritable.py new file mode 100644 index 00000000000..db0fcb2169c --- /dev/null +++ b/pyre/extra_tests/parity_tests/open_non_inheritable.py @@ -0,0 +1,27 @@ +import os +import tempfile + + +fd, path = tempfile.mkstemp() +os.close(fd) +try: + with open(path, "rb") as stream: + assert os.get_inheritable(stream.fileno()) is False + + def opener(name, flags): + return os.open(name, flags) + + with open(path, "rb", opener=opener) as stream: + assert os.get_inheritable(stream.fileno()) is False + + supplied = os.open(path, os.O_RDONLY) + try: + os.set_inheritable(supplied, True) + with open(supplied, "rb", closefd=False) as stream: + assert os.get_inheritable(stream.fileno()) is True + finally: + os.close(supplied) +finally: + os.unlink(path) + +print("OK") diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index 828f29313ae..dbab983287e 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -15119,6 +15119,10 @@ fn open_flags_for_mode(mode: &str) -> i32 { if exclusive { flags |= libc::O_CREAT | libc::O_EXCL; } + // PyPy `W_FileIO.descr_init`: every pathname/opener call receives + // `O_CLOEXEC` when the platform provides it. The explicit fcntl below + // remains necessary for an opener that ignores the supplied flag. + flags |= libc::O_CLOEXEC; flags } #[cfg(not(unix))] @@ -15147,6 +15151,23 @@ fn fileio_close_owned_fd(fd: i32) { let _ = crate::host_seam::ops::close(fd); } +/// PyPy `_open_inhcache.set_non_inheritable(fd)` / `rposix.set_inheritable`. +/// A caller-supplied integer descriptor is deliberately excluded: FileIO must +/// preserve that descriptor's existing inheritance flag. +#[cfg(all(unix, not(feature = "sandbox")))] +fn fileio_set_non_inheritable(fd: i32, w_name: PyObjectRef) -> Result<(), crate::PyError> { + let current = crt_call!(libc::fcntl(fd, libc::F_GETFD)); + if current < 0 { + return Err(crate::PyError::os_error_syscall(crt_errno(), w_name)); + } + if current & libc::FD_CLOEXEC == 0 + && crt_call!(libc::fcntl(fd, libc::F_SETFD, current | libc::FD_CLOEXEC)) < 0 + { + return Err(crate::PyError::os_error_syscall(crt_errno(), w_name)); + } + Ok(()) +} + /// `open()` — PyPy `pypy/module/_io/interp_io.py:open`. /// /// Keep the upstream construction order literal: validate the mode, create a @@ -15517,6 +15538,11 @@ fn open_raw_file(args: &[PyObjectRef]) -> Result { if fd < 0 { return Err(crate::PyError::value_error(format!("opener returned {fd}"))); } + #[cfg(all(unix, not(feature = "sandbox")))] + if let Err(error) = fileio_set_non_inheritable(fd, resolved_path.w_path()) { + fileio_close_owned_fd(fd); + return Err(error); + } #[cfg(unix)] if let Err(error) = fileio_validate_fd(fd, resolved_path.w_path()) { fileio_close_owned_fd(fd); @@ -15578,6 +15604,10 @@ fn open_raw_file(args: &[PyObjectRef]) -> Result { // wrapper from which it came. let fd = crate::host_seam::ops::open(path_bytes, flags, 0o666) .map_err(|e| crate::host_seam::seam_os_err_with_filename(e, resolved_path.w_path()))?; + if let Err(error) = fileio_set_non_inheritable(fd, resolved_path.w_path()) { + fileio_close_owned_fd(fd); + return Err(error); + } if let Err(error) = fileio_validate_fd(fd, resolved_path.w_path()) { fileio_close_owned_fd(fd); return Err(error); From 39e1ec8d861af111955868187969de0681ee0c84 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 06:18:57 +0900 Subject: [PATCH 17/26] exceptions: describe unraisable user finalizers --- .../parity_tests/unraisable_del_message.py | 26 +++++++++++++++++++ pyre/pyre-interpreter/src/executioncontext.rs | 15 +++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 pyre/extra_tests/parity_tests/unraisable_del_message.py diff --git a/pyre/extra_tests/parity_tests/unraisable_del_message.py b/pyre/extra_tests/parity_tests/unraisable_del_message.py new file mode 100644 index 00000000000..2b08148f0b8 --- /dev/null +++ b/pyre/extra_tests/parity_tests/unraisable_del_message.py @@ -0,0 +1,26 @@ +import gc +import sys + + +class BrokenDel: + def __del__(self): + raise ValueError("del is broken") + + +seen = [] +old_hook = sys.unraisablehook +sys.unraisablehook = seen.append +try: + obj = BrokenDel() + del_repr = repr(type(obj).__del__) + del obj + gc.collect() +finally: + sys.unraisablehook = old_hook + +assert len(seen) == 1 +assert seen[0].err_msg == f"Exception ignored while calling deallocator {del_repr}" +assert seen[0].exc_type is ValueError +assert seen[0].object is None + +print("OK") diff --git a/pyre/pyre-interpreter/src/executioncontext.rs b/pyre/pyre-interpreter/src/executioncontext.rs index 7ea2a3a85fb..bf53fe8d6d4 100644 --- a/pyre/pyre-interpreter/src/executioncontext.rs +++ b/pyre/pyre-interpreter/src/executioncontext.rs @@ -2397,11 +2397,22 @@ impl UserDelAction { if let Err(error) = unsafe { crate::baseobjspace::get_and_call_function(del(), current(), w_type.as_ptr(), &[]) } { + // PyPy executioncontext.py:680-690 passes an empty `where` and + // the `__del__` descriptor to `write_unraisable`. Python 3.14's + // `_PyErr_FormatUnraisable` gives this finalizer case a more + // specific hook message; 3.14 is pyre's compatibility target + // when its observable result differs from PyPy's. + let del_repr = unsafe { crate::display::py_repr_wtf8(del()) } + .unwrap_or_else(|_| rustpython_wtf8::Wtf8Buf::from_string("".to_string())); + let where_desc = crate::display::wtf8_format!( + "Exception ignored while calling deallocator ", + del_repr + ); report_error( self.base.space, &error, - rustpython_wtf8::Wtf8::new(""), - del(), + &where_desc, + pyre_object::w_none(), ); } } From 4dbc2719e22ae8cade7ca1cf3348c54e91ec6c73 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 06:29:37 +0900 Subject: [PATCH 18/26] function: inherit builtins from the caller frame --- .../function_builtins_inheritance.py | 38 +++++++++++++++++++ pyre/pyre-interpreter/src/function.rs | 33 ++++++++++++---- 2 files changed, 64 insertions(+), 7 deletions(-) create mode 100644 pyre/extra_tests/parity_tests/function_builtins_inheritance.py diff --git a/pyre/extra_tests/parity_tests/function_builtins_inheritance.py b/pyre/extra_tests/parity_tests/function_builtins_inheritance.py new file mode 100644 index 00000000000..dc7d9111bec --- /dev/null +++ b/pyre/extra_tests/parity_tests/function_builtins_inheritance.py @@ -0,0 +1,38 @@ +import types + + +def sample(value): + return len(value) + + +empty_globals = {} +inherited = types.FunctionType(sample.__code__, empty_globals) +builtins_dict = __builtins__.__dict__ if hasattr(__builtins__, "__dict__") else __builtins__ +assert inherited.__globals__ is empty_globals +assert inherited.__builtins__ is builtins_dict +assert inherited("abc") == 3 +assert empty_globals == {} + +safe_builtins = {"None": None} +namespace = {"type": type, "__builtins__": safe_builtins} +exec( + "def inner(): pass\n" + "cloned = type(inner)(inner.__code__, {})\n", + namespace, +) +assert namespace["inner"].__builtins__ is safe_builtins +assert namespace["cloned"].__builtins__ is safe_builtins +assert "__builtins__" not in namespace["cloned"].__globals__ + + +class GlobalsSubclass(dict): + def __getitem__(self, key): + raise AssertionError("function construction must use the dict backing") + + +custom_builtins = {"marker": object()} +subclass_globals = GlobalsSubclass(__builtins__=custom_builtins) +subclass_function = types.FunctionType(sample.__code__, subclass_globals) +assert subclass_function.__builtins__ is custom_builtins + +print("OK") diff --git a/pyre/pyre-interpreter/src/function.rs b/pyre/pyre-interpreter/src/function.rs index 671c1986d69..b9868d58d1b 100644 --- a/pyre/pyre-interpreter/src/function.rs +++ b/pyre/pyre-interpreter/src/function.rs @@ -506,16 +506,35 @@ pub(crate) fn function_new_impl( let globals_slot = pyre_object::gc_roots::shadow_stack_len(); pyre_object::gc_roots::pin_root(w_func_globals_obj); - // CPython 3.14 `_PyEval_BuiltinsFromGlobals` at function construction: + // CPython 3.14 `_PyDict_LoadBuiltinsFromGlobals` at function construction: // retain the selected mapping identity even if the globals entry changes - // later. Builtin/gateway carriers have no globals and keep a null slot. + // later. Its `_Py_dict_lookup_threadsafe_stackref` reads the native dict + // backing directly (including for a dict subclass), and a missing key + // inherits `PyEval_GetBuiltins()` from the live caller frame. This differs + // from PyPy's `pick_builtin`, whose missing-key result is a fresh anonymous + // module containing only `None`; 3.14 is pyre's compatibility target for + // the observable `function.__builtins__` field. + // + // Builtin/gateway carriers have no globals and keep a null slot. let w_builtins = if w_func_globals_obj.is_null() { PY_NULL } else { - let selected = crate::baseobjspace::pick_builtin_obj( - w_func_globals_obj, - crate::call::take_last_exec_ctx(), - ); + let globals_backing = crate::type_methods::resolve_dict_backing(w_func_globals_obj); + let selected = unsafe { + pyre_object::w_dict_getitem_str(globals_backing, "__builtins__") + } + .unwrap_or_else(|| { + let exec_ctx = crate::call::take_last_exec_ctx(); + if exec_ctx.is_null() { + return PY_NULL; + } + let frame = unsafe { (*exec_ctx).gettopframe_raw() }; + if frame.is_null() { + unsafe { (*exec_ctx).get_builtin_dict() } + } else { + unsafe { (*frame).fget_f_builtins() } + } + }); if !selected.is_null() && unsafe { pyre_object::is_module(selected) } { unsafe { pyre_object::w_module_get_w_dict(selected) } } else { @@ -525,7 +544,7 @@ pub(crate) fn function_new_impl( let builtins_slot = pyre_object::gc_roots::shadow_stack_len(); pyre_object::gc_roots::pin_root(w_builtins); - // `pick_builtin_obj` and later allocations may collect. Reload every + // Resolving the caller vref and later allocations may collect. Reload every // pinned input before embedding it in the new Function; the original raw // locals are not rewritten when the shadow-stack slots are forwarded. let closure = pyre_object::gc_roots::shadow_stack_get(closure_slot); From 624973a40b268fd899383c4a8f2b150b78829a02 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 06:40:26 +0900 Subject: [PATCH 19/26] exceptions: port BlockingIOError written slot --- .../blockingioerror_characters_written.py | 29 ++++++++++ pyre/pyre-interpreter/src/baseobjspace.rs | 54 ++++++++++++++++--- pyre/pyre-interpreter/src/builtins.rs | 13 +++-- pyre/pyre-object/src/interp_exceptions.rs | 26 +++++++++ 4 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 pyre/extra_tests/parity_tests/blockingioerror_characters_written.py diff --git a/pyre/extra_tests/parity_tests/blockingioerror_characters_written.py b/pyre/extra_tests/parity_tests/blockingioerror_characters_written.py new file mode 100644 index 00000000000..9e3e95e909a --- /dev/null +++ b/pyre/extra_tests/parity_tests/blockingioerror_characters_written.py @@ -0,0 +1,29 @@ +for count in range(6): + error = BlockingIOError(*("a", "b", "c", "d", "e")[:count]) + try: + error.characters_written + except AttributeError: + pass + else: + raise AssertionError("unset characters_written must be absent") + +error = BlockingIOError("a", "b", 3) +assert error.args == ("a", "b", 3) +assert error.characters_written == 3 +error.characters_written = 5 +assert error.characters_written == 5 +assert error.args == ("a", "b", 3) +del error.characters_written +try: + error.characters_written +except AttributeError: + pass +else: + raise AssertionError("deleted characters_written must be absent") + +plain = OSError() +plain.characters_written = 7 +assert plain.characters_written == 7 +del plain.characters_written + +print("OK") diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index f114a94653c..d32cbcc3a40 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -438,8 +438,9 @@ pub fn exception_getclass(w_obj: PyObjectRef) -> PyObjectRef { /// True when `obj` is a `BlockingIOError` whose constructor took the numeric /// third argument as `characters_written` — recognised by `args_w[2]` still /// being an int (every other 2..=5-argument form trims `args_w` to two -/// elements). Gates the `characters_written` reader and suppresses the -/// `filename` derivation for that argument (`interp_exceptions.py` `_init_error`). +/// elements). Suppresses `filename` derivation for that constructor argument +/// even after the independent `written` slot is later deleted +/// (`interp_exceptions.py` `_init_error`). fn exc_blocking_written(obj: PyObjectRef) -> bool { let args = unsafe { pyre_object::interp_exceptions::w_exception_get_args(obj) }; let n = unsafe { pyre_object::w_tuple_len(args) }; @@ -7246,10 +7247,16 @@ pub(crate) fn exception_attr_get(obj: PyObjectRef, name: &str) -> PyResult { // `BlockingIOError` constructed with a numeric third argument keeps // it in `args_w[2]` as `characters_written`; otherwise the slot is // unset (`written == -1`) and the attribute raises `AttributeError`. - "characters_written" if exc_blocking_written(obj) => { - let args = unsafe { pyre_object::interp_exceptions::w_exception_get_args(obj) }; - if let Some(v) = unsafe { pyre_object::w_tuple_getitem(args, 2) } { - return Ok(v); + "characters_written" => { + if crate::builtins::lookup_exc_class("OSError") + .is_some_and(|os_error| unsafe { isinstance_w(obj, os_error) }) + { + let written = unsafe { + pyre_object::interp_exceptions::w_exception_get_written(obj) + }; + if written != -1 { + return Ok(pyre_object::w_int_new(written)); + } } } // `interp_exceptions.py:409-411 W_ImportError` exposes @@ -11413,6 +11420,22 @@ pub(crate) fn exception_attr_set(obj: PyObjectRef, name: &str, value: PyObjectRe return Ok(w_none()); } } + // `interp_exceptions.py:709-711 W_OSError.descr_set_written` — the + // descriptor is declared on OSError (and therefore applies to every + // subclass), converts through `space.int_w`, and stores independently + // from the constructor args tuple. + "characters_written" => { + let Some(os_error) = crate::builtins::lookup_exc_class("OSError") else { + return Ok(pyre_object::PY_NULL); + }; + if unsafe { isinstance_w(obj, os_error) } { + let written = int_w(value)?; + unsafe { + pyre_object::interp_exceptions::w_exception_set_written(obj, written) + }; + return Ok(w_none()); + } + } // `interp_exceptions.py:723-728`: the `winerror` descriptor is // installed only where the platform has Windows error codes, so // elsewhere the name falls through to the ordinary instance dict. @@ -12324,6 +12347,25 @@ pub(crate) fn exception_attr_delete(obj: PyObjectRef, name: &str) -> PyResult { { return Err(PyError::type_error("can't delete numeric/char attribute")); } + // `interp_exceptions.py:713-715 W_OSError.descr_del_written` — an + // already-unset slot raises; otherwise deletion restores `-1`. + "characters_written" => { + let Some(os_error) = crate::builtins::lookup_exc_class("OSError") else { + return Ok(pyre_object::PY_NULL); + }; + if unsafe { isinstance_w(obj, os_error) } { + let written = unsafe { + pyre_object::interp_exceptions::w_exception_get_written(obj) + }; + if written == -1 { + return Err(PyError::attribute_error("characters_written")); + } + unsafe { + pyre_object::interp_exceptions::w_exception_set_written(obj, -1) + }; + return Ok(w_none()); + } + } _ if unsafe { exception_deletable_slot(obj, name) } => { return object_setattr(obj, name, w_none()); } diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index dbab983287e..97e10933833 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -6042,8 +6042,7 @@ fn exception_args_already(w_self: PyObjectRef, positional: &[PyObjectRef]) -> bo /// A 2..=5 positional-argument call fills the `errno` / `strerror` / /// `filename` / `filename2` slots; when a filename is present it is /// dropped from `args_w` (`self.args_w = [w_errno, w_strerror]`, line -/// 652) for pickle / repr compatibility. The `BlockingIOError.written` -/// special-case is not modelled. `kind` is `OSError` for the base type and +/// 652) for pickle / repr compatibility. `kind` is `OSError` for the base type and /// `FileNotFoundError` for that dedicated kind; every other OSError subclass /// routes here as `OSError` with its `w_class` retagged by `exc_new_wrapper!`. fn os_error_build( @@ -6248,8 +6247,14 @@ fn os_error_fill_slots(exc: PyObjectRef, args: &[PyObjectRef]) { // `_init_error` line 636-643: for an exact `BlockingIOError`, a // numeric third argument is `characters_written`, not a filename — // it stays in `args_w` and the tuple is not trimmed. - let written = |f| exc_is_blocking_io_error(exc) && pyre_object::is_int(f); - if let Some(fname) = w_filename.filter(|&f| !written(f)) { + let written = |f| { + exc_is_blocking_io_error(exc) + .then(|| crate::baseobjspace::int_w(f).ok()) + .flatten() + }; + if let Some(value) = w_filename.and_then(written) { + interp_exceptions::w_exception_set_written(exc, value); + } else if let Some(fname) = w_filename { interp_exceptions::w_exception_set_filename(exc, fname); if let Some(f2) = args.get(4).copied().filter(|&f| !pyre_object::is_none(f)) { interp_exceptions::w_exception_set_filename2(exc, f2); diff --git a/pyre/pyre-object/src/interp_exceptions.rs b/pyre/pyre-object/src/interp_exceptions.rs index 104ef359757..3c9027868b1 100644 --- a/pyre/pyre-object/src/interp_exceptions.rs +++ b/pyre/pyre-object/src/interp_exceptions.rs @@ -382,6 +382,11 @@ pub struct W_BaseException { /// `interp_exceptions.py:527 W_OSError.w_filename2` / /// `:742 readwrite_attrproperty_w('w_filename2', W_OSError)`. pub w_filename2: PyObjectRef, + /// `interp_exceptions.py:528 W_OSError.written = -1` — the independent + /// integer slot exposed by the `characters_written` GetSetProperty. + /// A numeric third argument on an exact BlockingIOError stamps it; later + /// descriptor writes and deletes mutate this slot without changing args. + pub written: i64, /// `interp_exceptions.py:990 W_SystemExit.w_code` / /// `:1006 readwrite_attrproperty_w('w_code', W_SystemExit)`. /// `PY_NULL` is the class default `None`; the `code` getattr arm @@ -699,6 +704,8 @@ fn w_exception_new_empty_impl(kind: ExcKind, immortal: bool) -> PyObjectRef { w_strerror: PY_NULL, w_filename: PY_NULL, w_filename2: PY_NULL, + // `interp_exceptions.py:528` W_OSError class default. + written: -1, // `interp_exceptions.py:990` W_SystemExit class default // `w_code = None`. w_code: PY_NULL, @@ -1364,6 +1371,25 @@ pub unsafe fn w_exception_set_filename2(obj: PyObjectRef, value: PyObjectRef) { } } +/// `interp_exceptions.py:704-715 W_OSError.descr_{get,set,del}_written`. +/// `-1` is the unset sentinel; the descriptor converts values before storing. +/// +/// # Safety +/// `obj` must point to a valid `W_BaseException`. +#[inline] +pub unsafe fn w_exception_get_written(obj: PyObjectRef) -> i64 { + unsafe { (*(obj as *const W_BaseException)).written } +} + +/// Store the `W_OSError.written` integer slot. +/// +/// # Safety +/// `obj` must point to a valid `W_BaseException`. +#[inline] +pub unsafe fn w_exception_set_written(obj: PyObjectRef, value: i64) { + unsafe { (*(obj as *mut W_BaseException)).written = value }; +} + /// `interp_exceptions.py:1006 readwrite_attrproperty_w('w_code', ...)` /// — `e.code` reader. `PY_NULL` means the slot was never written (the /// `code` getattr arm then derives the value from `args_w`). From 3a535a4641267166f44ec75bbed2629bb71e73d6 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 06:47:48 +0900 Subject: [PATCH 20/26] socket: alias timeout to builtin TimeoutError --- .../parity_tests/socket_timeout_identity.py | 9 ++++++++ .../src/module/_socket/interp_socket.rs | 22 +++++++------------ 2 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 pyre/extra_tests/parity_tests/socket_timeout_identity.py diff --git a/pyre/extra_tests/parity_tests/socket_timeout_identity.py b/pyre/extra_tests/parity_tests/socket_timeout_identity.py new file mode 100644 index 00000000000..30a122169f8 --- /dev/null +++ b/pyre/extra_tests/parity_tests/socket_timeout_identity.py @@ -0,0 +1,9 @@ +import _socket +import socket + + +assert _socket.timeout is TimeoutError +assert socket.timeout is TimeoutError +assert socket.timeout.__module__ == "builtins" + +print("OK") diff --git a/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs b/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs index aed777dab86..97854973048 100644 --- a/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs +++ b/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs @@ -1121,11 +1121,11 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { ); } - // `interp_socket.py:1041-1063 SocketAPI`: - // error = w_OSError (alias) - // herror = new_exception_class("_socket.herror", w_OSError) - // gaierror = new_exception_class("_socket.gaierror", w_OSError) - // timeout = new_exception_class("_socket.timeout", w_OSError) + // `moduledef.py:12-16`: + // error = get_error(space, "error") + // herror = get_error(space, "herror") + // gaierror = get_error(space, "gaierror") + // timeout = space.w_TimeoutError // `socketmodule.c` names them `socket.herror` / `socket.gaierror` // instead, and `type.__module__` reads the qualified prefix back, so // `socket.gaierror.__module__` is `"socket"` rather than `"_socket"`. @@ -1150,15 +1150,9 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { w_os_error, ), ); - crate::module_ns_store( - ns, - "timeout", - crate::builtins::make_exc_type( - "socket.timeout", - crate::builtins::exc_exception_new, - w_os_error, - ), - ); + let w_timeout_error = crate::builtins::lookup_exc_class("TimeoutError") + .expect("TimeoutError must be installed before _socket init"); + crate::module_ns_store(ns, "timeout", w_timeout_error); // Default timeout (None) — modulus has a getter/setter; we just stash // a None so attribute lookups succeed. From 25707aea8fcd76f06f1a2cd0942ec137a8be9e76 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 10:23:28 +0900 Subject: [PATCH 21/26] jit: preserve LOAD_GLOBAL null in guard snapshots --- .../src/jitcode_dispatch/mod.rs | 6 ++ .../src/jitcode_dispatch/resume_snapshot.rs | 15 +++-- .../src/jitcode_dispatch/tests.rs | 22 ++++++++ .../src/jitcode_dispatch/vstack_mirror.rs | 55 ++++++++++++++----- 4 files changed, 79 insertions(+), 19 deletions(-) diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs index 4ac9500e1ce..b1306d31ae9 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs @@ -6521,6 +6521,12 @@ enum VstackOpClass { /// BINARY_SUBSCR / COMPARE_OP / unary ops / CALL / /// IS_OP / CONTAINS_OP / single-result BUILD_*. ResultToTos, + /// Pyre's method-form `LOAD_GLOBAL` pushes the callable and then a NULL + /// `self_or_null` sentinel. The callable is recovered from the + /// virtualizable shadow; the top slot is an explicit Ref constant so a + /// guard inside the following CALL can snapshot the live NULL instead of + /// inheriting an older value from that frame slot. + LoadGlobalMethod, /// The opcode only pops (and/or stores to a local/global/attr/subscr, /// or is an unconditional control transfer). Truncate to the new /// depth WITHOUT touching the surviving TOS — the box already in that diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs index 7c3f5fcfe37..1c08f4cb2a1 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs @@ -55,6 +55,10 @@ fn forward_snapshot_py_pc(jitcode_index: u32, pc: u32) -> Result Option { + (value != OpRef::NONE).then_some(value) +} + /// `generate_guard` (`pyjitpl.py`) keys `after_residual_call` /// on the guard opcode itself: `GUARD_EXCEPTION` / `GUARD_NO_EXCEPTION` / /// `GUARD_NOT_FORCED` / `GUARD_ALWAYS_FAILS` resume *after* the residual @@ -660,7 +664,10 @@ pub(crate) fn walker_capture_snapshot_for_last_guard_impl( // backends — so a slot the mirror does not cover (invalid // mirror, slot beyond the mirror, or an Int-bank temp the // Ref-only mirror leaves NONE) is simply omitted; resume - // re-materializes it rather than reading the flat color. + // re-materializes it rather than reading the flat color. A + // ConstPtr(NULL) is not a hole: it is CALL's live + // `self_or_null` operand and must overwrite a stale shadow + // slot in the capture-only overlay. (0..depth) .filter_map(|s| { let v = if ctx.vstack_valid { @@ -668,11 +675,7 @@ pub(crate) fn walker_capture_snapshot_for_last_guard_impl( } else { OpRef::NONE }; - if v != OpRef::NONE && !opref_is_null_const_ptr(v) { - Some((nvs + nlocals + s, v)) - } else { - None - } + vstack_box_for_snapshot(v).map(|v| (nvs + nlocals + s, v)) }) .collect() } else { diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs index 96414549b6d..ba817110c41 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs @@ -85,6 +85,28 @@ fn after_residual_guard_uses_trailing_live_before_fallthrough_twin() { ); } +#[test] +fn method_load_global_snapshot_preserves_live_null_call_slot() { + let mut tc = TraceCtx::for_test_types(&[Type::Ref]); + let kept = tc.const_ref(41); + let stale = tc.const_ref(42); + let null = tc.const_null(); + let mut boxes = vec![kept, stale, stale]; + + super::vstack_mirror::reconcile_load_global_method_shape(&mut boxes, 1, 3, null); + + assert_eq!(boxes, vec![kept, OpRef::NONE, null]); + assert_eq!( + super::resume_snapshot::vstack_box_for_snapshot(null), + Some(null), + "CALL's NULL self slot must clear a stale vable shadow value", + ); + assert_eq!( + super::resume_snapshot::vstack_box_for_snapshot(OpRef::NONE), + None, + ); +} + #[test] fn vstack_permuted_for_iter_entry_uses_block_head_target() { let mut pyjit = crate::PyJitCode::skeleton(std::ptr::null()); diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs index 1d604a617c4..9571eaa4618 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs @@ -223,19 +223,13 @@ pub(crate) fn classify_vstack_opcode( | Instruction::JumpBackwardNoInterrupt { .. } | Instruction::ReturnValue => VstackOpClass::PopOnlyOrSideStore, - // LOAD_GLOBAL: the global value is the new TOS = the last Ref written. - // When `namei & 1` the lowering also pushes a NULL sentinel BENEATH the - // result (net +2, for the upcoming method CALL). Exactly like the - // two-push `LoadFast*LoadFast*` super-instructions, that leaves the slot - // below the new TOS a NONE hole which the general hole-fill below - // recovers from the virtualizable shadow (or leaves NONE when - // unsourceable — the overlay then omits the slot, which resume - // re-materializes) WITHOUT invalidating the mirror. The NULL sentinel - // is consumed by the CALL before any short-circuit branch guard, so it - // is never a live kept-stack slot at a resume. (Previously the - // `namei & 1` arm declined to `Unmodeled`; the hole-fill makes that - // unnecessary, and the decline killed the mirror for the rest of any - // walk with a method-form global load — the dominant mirror=NONE gap.) + // LOAD_GLOBAL: pyre pushes the global and then, for `namei & 1`, the + // NULL call sentinel. A guard emitted while lowering the following + // CALL can observe both slots, so method form keeps a distinct shape + // instead of treating NULL as an unsourceable result hole. + Instruction::LoadGlobal { namei } if namei.get(op_arg) & 1 != 0 => { + VstackOpClass::LoadGlobalMethod + } Instruction::LoadGlobal { .. } => VstackOpClass::ResultToTos, // LOAD_SUPER_ATTR: the attribute (non-method form) is the sole new TOS. @@ -357,6 +351,29 @@ fn loadconst_operand_ref( ctx.trace_ctx.const_ref(w_const as i64) } +/// Apply pyre's two-push method-form `LOAD_GLOBAL` stack shape. The callable +/// slot remains a hole until the ordinary shadow fill below supplies its box; +/// the following NULL is already a complete Ref constant and must not be +/// confused with an unresolved `OpRef::NONE` slot. +pub(crate) fn reconcile_load_global_method_shape( + boxes: &mut Vec, + old_depth: usize, + new_depth: usize, + null: OpRef, +) { + let old_depth = old_depth.min(new_depth); + boxes.truncate(new_depth); + if boxes.len() < new_depth { + boxes.resize(new_depth, OpRef::NONE); + } + for slot in &mut boxes[old_depth..new_depth] { + *slot = OpRef::NONE; + } + if new_depth > old_depth { + boxes[new_depth - 1] = null; + } +} + /// #73: reconcile the PREVIOUS Python opcode's stack effect into /// [`WalkContext::vstack_boxes`] at an opcode boundary, BEFORE the new /// opcode (`new_pypc`) is walked. Running this before the new op means @@ -582,6 +599,18 @@ pub(crate) fn reconcile_vstack_at_boundary( ctx.vstack_boxes[new_depth - 1] = top; } } + VstackOpClass::LoadGlobalMethod => { + // `opcode_load_global`: push the callable, then NULL. The + // callable's vable store fills the preceding hole below; NULL is + // semantically live even though it carries no GC pointer. + let null = ctx.trace_ctx.const_null(); + reconcile_load_global_method_shape( + &mut ctx.vstack_boxes, + ctx.vstack_depth, + new_depth, + null, + ); + } VstackOpClass::PopOnlyOrSideStore => { ctx.vstack_boxes.truncate(new_depth); } From dedc639fde81bfbd167ce5835a902ed8a88540d1 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Sun, 9 Aug 2026 21:46:37 +0900 Subject: [PATCH 22/26] baseobjspace, executioncontext, function: reformat six rustfmt-reflowed sites `cargo fmt --all -- --check` reported four hunks in `baseobjspace.rs` and one each in `executioncontext.rs` and `function.rs`: `unsafe { ... }` blocks and call argument lists that rustfmt joins onto fewer lines. Whitespace only. Assisted-by: Claude --- pyre/pyre-interpreter/src/baseobjspace.rs | 18 ++++-------- pyre/pyre-interpreter/src/executioncontext.rs | 7 +---- pyre/pyre-interpreter/src/function.rs | 28 +++++++++---------- 3 files changed, 20 insertions(+), 33 deletions(-) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index d32cbcc3a40..03b80e65352 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -7251,9 +7251,8 @@ pub(crate) fn exception_attr_get(obj: PyObjectRef, name: &str) -> PyResult { if crate::builtins::lookup_exc_class("OSError") .is_some_and(|os_error| unsafe { isinstance_w(obj, os_error) }) { - let written = unsafe { - pyre_object::interp_exceptions::w_exception_get_written(obj) - }; + let written = + unsafe { pyre_object::interp_exceptions::w_exception_get_written(obj) }; if written != -1 { return Ok(pyre_object::w_int_new(written)); } @@ -11430,9 +11429,7 @@ pub(crate) fn exception_attr_set(obj: PyObjectRef, name: &str, value: PyObjectRe }; if unsafe { isinstance_w(obj, os_error) } { let written = int_w(value)?; - unsafe { - pyre_object::interp_exceptions::w_exception_set_written(obj, written) - }; + unsafe { pyre_object::interp_exceptions::w_exception_set_written(obj, written) }; return Ok(w_none()); } } @@ -12354,15 +12351,12 @@ pub(crate) fn exception_attr_delete(obj: PyObjectRef, name: &str) -> PyResult { return Ok(pyre_object::PY_NULL); }; if unsafe { isinstance_w(obj, os_error) } { - let written = unsafe { - pyre_object::interp_exceptions::w_exception_get_written(obj) - }; + let written = + unsafe { pyre_object::interp_exceptions::w_exception_get_written(obj) }; if written == -1 { return Err(PyError::attribute_error("characters_written")); } - unsafe { - pyre_object::interp_exceptions::w_exception_set_written(obj, -1) - }; + unsafe { pyre_object::interp_exceptions::w_exception_set_written(obj, -1) }; return Ok(w_none()); } } diff --git a/pyre/pyre-interpreter/src/executioncontext.rs b/pyre/pyre-interpreter/src/executioncontext.rs index bf53fe8d6d4..f54386c7d4d 100644 --- a/pyre/pyre-interpreter/src/executioncontext.rs +++ b/pyre/pyre-interpreter/src/executioncontext.rs @@ -2408,12 +2408,7 @@ impl UserDelAction { "Exception ignored while calling deallocator ", del_repr ); - report_error( - self.base.space, - &error, - &where_desc, - pyre_object::w_none(), - ); + report_error(self.base.space, &error, &where_desc, pyre_object::w_none()); } } } diff --git a/pyre/pyre-interpreter/src/function.rs b/pyre/pyre-interpreter/src/function.rs index b9868d58d1b..aba55d35ac7 100644 --- a/pyre/pyre-interpreter/src/function.rs +++ b/pyre/pyre-interpreter/src/function.rs @@ -520,21 +520,19 @@ pub(crate) fn function_new_impl( PY_NULL } else { let globals_backing = crate::type_methods::resolve_dict_backing(w_func_globals_obj); - let selected = unsafe { - pyre_object::w_dict_getitem_str(globals_backing, "__builtins__") - } - .unwrap_or_else(|| { - let exec_ctx = crate::call::take_last_exec_ctx(); - if exec_ctx.is_null() { - return PY_NULL; - } - let frame = unsafe { (*exec_ctx).gettopframe_raw() }; - if frame.is_null() { - unsafe { (*exec_ctx).get_builtin_dict() } - } else { - unsafe { (*frame).fget_f_builtins() } - } - }); + let selected = unsafe { pyre_object::w_dict_getitem_str(globals_backing, "__builtins__") } + .unwrap_or_else(|| { + let exec_ctx = crate::call::take_last_exec_ctx(); + if exec_ctx.is_null() { + return PY_NULL; + } + let frame = unsafe { (*exec_ctx).gettopframe_raw() }; + if frame.is_null() { + unsafe { (*exec_ctx).get_builtin_dict() } + } else { + unsafe { (*frame).fget_f_builtins() } + } + }); if !selected.is_null() && unsafe { pyre_object::is_module(selected) } { unsafe { pyre_object::w_module_get_w_dict(selected) } } else { From ef7dda165c9ea7441c88b58c04713bda2e6a7ca7 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 10 Aug 2026 03:09:42 +0900 Subject: [PATCH 23/26] builtins: port mixed numeric lists and fix JIT regressions --- .../src/aarch64/assembler.rs | 4 +- .../majit-backend-dynasm/src/x86/assembler.rs | 4 +- majit/majit-metainterp/src/blackhole.rs | 43 +- majit/majit-metainterp/src/pyjitpl.rs | 122 ++++- pyre/bench/synth/pypy_type_surface.py | 58 +-- .../blockingioerror_characters_written.py | 7 + .../parity_tests/re_jit_call_resume.py | 7 + pyre/pyre-interpreter/src/baseobjspace.rs | 20 +- pyre/pyre-interpreter/src/builtins.rs | 4 + pyre/pyre-interpreter/src/call.rs | 11 +- pyre/pyre-interpreter/src/function.rs | 30 +- pyre/pyre-interpreter/src/module/sys/vm.rs | 29 +- pyre/pyre-interpreter/src/pyframe.rs | 17 +- pyre/pyre-interpreter/src/type_methods.rs | 5 +- pyre/pyre-interpreter/src/typedef.rs | 55 +- pyre/pyre-jit-trace/src/descr.rs | 14 + pyre/pyre-jit-trace/src/helpers.rs | 11 +- .../src/jitcode_dispatch/fbw_state.rs | 21 + .../src/jitcode_dispatch/specialize.rs | 5 + .../src/jitcode_dispatch/tests.rs | 32 +- pyre/pyre-object/src/float_array.rs | 2 +- pyre/pyre-object/src/int_array.rs | 2 +- pyre/pyre-object/src/interp_exceptions.rs | 16 + pyre/pyre-object/src/listobject.rs | 485 +++++++++++++++++- 24 files changed, 826 insertions(+), 178 deletions(-) create mode 100644 pyre/extra_tests/parity_tests/re_jit_call_resume.py diff --git a/majit/majit-backend-dynasm/src/aarch64/assembler.rs b/majit/majit-backend-dynasm/src/aarch64/assembler.rs index db83bbdd1e3..d9a9ac0091e 100644 --- a/majit/majit-backend-dynasm/src/aarch64/assembler.rs +++ b/majit/majit-backend-dynasm/src/aarch64/assembler.rs @@ -2215,9 +2215,9 @@ impl<'a> AssemblerARM64<'a> { // opref_to_slot stores ABSOLUTE jitframe slots (user position + // JITFRAME_FIXED_SIZE) so slot_offset(slot) gives the correct byte // offset without further adjustment. - for iarg in inputargs { + for (position, iarg) in inputargs.iter().enumerate() { self.opref_to_slot - .insert(iarg.opref(), JITFRAME_FIXED_SIZE + iarg.index as usize); + .insert(iarg.opref(), JITFRAME_FIXED_SIZE + position); } // Also sync any frame allocations from regalloc's FrameManager. for (&opref, lifetime) in ra.longevity.lifetimes_iter() { diff --git a/majit/majit-backend-dynasm/src/x86/assembler.rs b/majit/majit-backend-dynasm/src/x86/assembler.rs index 41fda06c720..c2acfa70b4a 100644 --- a/majit/majit-backend-dynasm/src/x86/assembler.rs +++ b/majit/majit-backend-dynasm/src/x86/assembler.rs @@ -2848,9 +2848,9 @@ impl<'a> Assembler386<'a> { // opref_to_slot stores ABSOLUTE jitframe slots (user position + // JITFRAME_FIXED_SIZE) so slot_offset(slot) gives the correct byte // offset without further adjustment. - for iarg in inputargs { + for (position, iarg) in inputargs.iter().enumerate() { self.opref_to_slot - .insert(iarg.opref(), JITFRAME_FIXED_SIZE + iarg.index as usize); + .insert(iarg.opref(), JITFRAME_FIXED_SIZE + position); } // Also sync any frame allocations from regalloc's FrameManager. for (&opref, lifetime) in ra.longevity.lifetimes_iter() { diff --git a/majit/majit-metainterp/src/blackhole.rs b/majit/majit-metainterp/src/blackhole.rs index 3d3cd77dc9b..ef86084e0dc 100644 --- a/majit/majit-metainterp/src/blackhole.rs +++ b/majit/majit-metainterp/src/blackhole.rs @@ -1396,6 +1396,23 @@ impl BlackholeInterpreter { } if op == self.op_live { crossed_trailing_live = true; + continue; + } + if !matches!( + op, + majit_translate::insns::BC_SETFIELD_VABLE_I + | majit_translate::insns::BC_SETFIELD_VABLE_R + | majit_translate::insns::BC_SETFIELD_VABLE_F + | majit_translate::insns::BC_SETARRAYITEM_VABLE_I + | majit_translate::insns::BC_SETARRAYITEM_VABLE_R + | majit_translate::insns::BC_SETARRAYITEM_VABLE_F + ) { + // Only the codewriter's successor-block virtualizable mirror + // stores may separate the guard resume coordinate from the + // raising operation's trailing live marker. Crossing an + // arbitrary operation would attach its exception to the next + // operation's handler and silently swallow it. + return None; } } None @@ -4324,12 +4341,13 @@ mod tests { fn test_guard_exception_resume_finds_catch_after_successor_sync() { let mut asm = majit_translate::codewriter::assembler::Assembler::new(); let mut b = JitCodeBuilder::default(); + b.load_const_r_value(0, 1); + b.load_const_i_value(0, 2); let resume_pc = b.current_pos(); b.live(&mut asm, &[], &[], &[]); // The normal-flow successor mirrors virtualizable state before // the can-raise block's trailing live/catch pair. - b.load_const_i_value(0, 1); - b.load_const_i_value(1, 2); + b.vable_setfield_int_with_base(0, 0, 0); b.live(&mut asm, &[], &[], &[]); let handler_lbl = b.new_label(); b.catch_exception(handler_lbl); @@ -4349,6 +4367,27 @@ mod tests { assert_eq!(bh.position, handler_pc); } + #[test] + fn test_guard_exception_resume_does_not_cross_another_operation() { + let mut asm = majit_translate::codewriter::assembler::Assembler::new(); + let mut b = JitCodeBuilder::default(); + let resume_pc = b.current_pos(); + b.live(&mut asm, &[], &[], &[]); + b.load_const_i_value(0, 1); + b.live(&mut asm, &[], &[], &[]); + let handler_lbl = b.new_label(); + b.catch_exception(handler_lbl); + b.mark_label(handler_lbl); + b.int_return(0); + let jitcode = b.finish(); + + let mut builder = super::build_inline_call_only_bh_builder(); + let mut bh = builder.acquire_interp(); + bh.setposition(std::sync::Arc::new(jitcode), resume_pc); + + assert!(!bh.handle_exception_in_frame(0xCAFE_F00D)); + } + thread_local! { /// Address of the interpreter's `exception_last_value` slot, read /// back by [`probe_exception_slot_at_record_time`]. diff --git a/majit/majit-metainterp/src/pyjitpl.rs b/majit/majit-metainterp/src/pyjitpl.rs index 415bb876fb5..20506615165 100644 --- a/majit/majit-metainterp/src/pyjitpl.rs +++ b/majit/majit-metainterp/src/pyjitpl.rs @@ -806,6 +806,62 @@ fn normalize_root_loop_entry_contract( Ok((inputargs, optimized_ops)) } +/// Backend handoff for compile.py:312 +/// `loop.inputargs = start_state.renamed_inputargs`. +/// +/// RPython passes box objects, whose identity is independent of the runtime +/// argument-vector position. Pyre's backend wire format instead uses a dense +/// InputArg payload for that position. Rebind the renamed boxes and *every* +/// use (ordinary args and guard failargs) together at this final boundary; +/// renumbering only the input list makes the same RPython box appear as two +/// different SSA values and reads an unrelated jitframe slot. +fn densify_root_loop_inputargs( + args: &[OpRef], + ops: Vec, +) -> (Vec, Vec) { + let mut replacements: indexmap::IndexMap = + indexmap::IndexMap::new(); + let inputargs = args + .iter() + .enumerate() + .map(|(position, &opref)| { + let tp = opref.ty().unwrap_or_else(|| { + panic!( + "renamed inputarg {:?} has no intrinsic type \ + (history.py:220 Box.type invariant)", + opref + ) + }); + let dense = std::rc::Rc::new(InputArg::from_type(tp, position as u32)); + replacements.insert(opref, dense.clone()); + InputArg::from_type(tp, position as u32) + }) + .collect(); + + let remap = |operand: &majit_ir::operand::Operand| { + replacements + .get(&operand.to_opref()) + .map(majit_ir::operand::Operand::from_bound_inputarg) + .unwrap_or_else(|| operand.clone()) + }; + let ops = ops + .into_iter() + .map(|op| { + let args = op + .getarglist() + .iter() + .map(&remap) + .collect::>(); + let cloned = std::rc::Rc::new(op.copy_and_change(op.opcode, Some(&args), None)); + if let Some(failargs) = op.getfailargs() { + cloned.setfailargs(failargs.iter().map(&remap).collect()); + } + cloned + }) + .collect(); + (inputargs, ops) +} + /// Slice T-final.F.0 survey probe. /// pub(crate) struct CompiledEntry { @@ -6318,11 +6374,11 @@ impl MetaInterp { // root inputarg types. RPython has no synthetic recovery when this // is absent; abort compilation so the caller falls back to the // interpreter instead of synthesizing Int-padded InputArgs. - let root_inputargs: Vec = if retried_without_unroll { + let renamed_root_args = if retried_without_unroll { // compile.py:233 compile_simple_loop: loop.inputargs is the // original trace inputargs. There is no ExportedState on the // simple path. - trace.inputargs_cloned() + None } else { match unroll_opt .final_exported_state @@ -6330,21 +6386,7 @@ impl MetaInterp { .map(|es| es.renamed_inputargs.as_slice()) .filter(|args| args.len() == final_num_inputs) { - Some(args) => args - .iter() - .enumerate() - .map(|(i, arg)| { - let opref = *arg; - let tp = opref.ty().unwrap_or_else(|| { - panic!( - "renamed inputarg {:?} has no intrinsic type \ - (history.py:220 Box.type invariant)", - opref - ) - }); - InputArg::from_type(tp, i as u32) - }) - .collect::>(), + Some(args) => Some(args.to_vec()), None => { if crate::majit_log_enabled() { eprintln!( @@ -6360,7 +6402,10 @@ impl MetaInterp { } } }; - let mut optimized_ops = optimized_ops; + let (root_inputargs, mut optimized_ops) = match renamed_root_args { + Some(args) => densify_root_loop_inputargs(&args, optimized_ops), + None => (trace.inputargs_cloned(), optimized_ops), + }; if retried_without_unroll && !optimized_ops .first() @@ -21324,6 +21369,47 @@ mod tests { assert_eq!(err, (0, 2)); } + #[test] + fn test_root_inputargs_and_uses_are_densified_together() { + // TraceIterator freshens boxes into a disjoint namespace. The backend + // argument vector is positional, so its handoff must rewrite both the + // declarations and every use of those renamed boxes. + let renamed = vec![ + OpRef::input_arg_ref(425), + OpRef::input_arg_int(596), + OpRef::input_arg_ref(614), + ]; + let guard = std::rc::Rc::new(mk_op( + OpCode::GuardClass, + &[renamed[0], OpRef::const_ptr(majit_ir::GcRef(0x1234))], + OpRef::NONE.raw(), + )); + guard.setfailargs( + [renamed[2]] + .into_iter() + .map(majit_ir::operand::Operand::bound_from_opref) + .collect(), + ); + let (inputargs, ops) = densify_root_loop_inputargs(&renamed, vec![guard]); + assert_eq!( + inputargs.iter().map(InputArg::opref).collect::>(), + vec![ + OpRef::input_arg_ref(0), + OpRef::input_arg_int(1), + OpRef::input_arg_ref(2), + ] + ); + assert_eq!( + inputargs.iter().map(|arg| arg.tp).collect::>(), + vec![Type::Ref, Type::Int, Type::Ref] + ); + assert_eq!(ops[0].arg(0).to_opref(), OpRef::input_arg_ref(0)); + assert_eq!( + ops[0].getfailargs().unwrap()[0].to_opref(), + OpRef::input_arg_ref(2) + ); + } + #[test] fn test_prepare_bridge_trace_for_optimizer_freshens_inputargs_and_snapshots() { let bridge_inputargs = vec![InputArg::new_int(0), InputArg::new_ref(1)]; diff --git a/pyre/bench/synth/pypy_type_surface.py b/pyre/bench/synth/pypy_type_surface.py index 4a892725518..7dcfac310e0 100644 --- a/pyre/bench/synth/pypy_type_surface.py +++ b/pyre/bench/synth/pypy_type_surface.py @@ -1,21 +1,9 @@ # pyre-check: no-cpython -# Every assertion here is a place the reference has something pyre deliberately -# does not, so the reference cannot be an oracle for this fixture: it is the -# side being diverged from. pypy is. -# -# `type.__basicsize__` and its three siblings describe a C struct behind a type -# and `W_TypeObject.typedef` (objspace/std/typeobject.py) exposes neither them -# nor a descriptor for them; `__flags__` there is a six-bit mask built by -# `get_flags` and reached through a `GetSetProperty`, not a full `tp_flags` -# word behind a member descriptor; `__sizeof__` exists on no typedef at all and -# `vm.py getsizeof` returns its default or raises. -import sys - +# Every assertion here is a place where PyPy remains pyre's reference rather +# than CPython. CPython 3.14 layout members and `__sizeof__` are now deliberate +# pyre compatibility surfaces and therefore no longer belong in this fixture. N = 20000 -LAYOUT_MEMBERS = ("__basicsize__", "__itemsize__", "__weakrefoffset__", "__dictoffset__") - - class Heap: pass @@ -24,18 +12,6 @@ class Derived(Heap): pass -def check_layout_members_absent(): - seen = 0 - for name in LAYOUT_MEMBERS: - if name in type.__dict__: - raise AssertionError(f"type.__dict__ carries {name}") - for owner in (object, type, int, str, tuple, BaseException, Heap): - if hasattr(owner, name): - raise AssertionError(f"{owner.__name__} carries {name}") - seen += 1 - return seen - - def check_flags(): # `get_flags`: _HEAPTYPE 1<<9, PATMA_SEQUENCE 1<<5, PATMA_MAPPING 1<<6, # _ABSTRACT 1<<20, Py_TPFLAGS_METHOD_DESCRIPTOR 1<<17. _CPYTYPE marks @@ -73,18 +49,6 @@ def check_descriptor_kinds(): return 2 -def check_sizeof_absent(): - owners = (object, type, int, str, list, dict, bytearray, set, frozenset) - seen = 0 - for owner in owners: - if "__sizeof__" in owner.__dict__: - raise AssertionError(f"{owner.__name__} carries __sizeof__") - seen += 1 - if hasattr(object, "__sizeof__"): - raise AssertionError("object exposes __sizeof__") - return seen - - def check_exception_group_immutable(): # `moduledef.py` names `W_ExceptionGroup` under `interpleveldefs`, so # `ExceptionGroup` is a static builtin beside `BaseExceptionGroup` rather @@ -101,29 +65,13 @@ def check_exception_group_immutable(): raise AssertionError("ExceptionGroup accepted an attribute assignment") -def check_getsizeof(): - sentinel = object() - if sys.getsizeof(sentinel, sentinel) is not sentinel: - raise AssertionError("getsizeof did not return its default by identity") - try: - sys.getsizeof(sentinel) - except TypeError as error: - if str(error) != sys.getsizeof.__doc__: - raise AssertionError("getsizeof message is not its docstring") - return 1 - raise AssertionError("getsizeof accepted a single argument") - - def main(): acc = 0 i = 0 while i < N: - acc += check_layout_members_absent() acc += check_flags() acc += check_descriptor_kinds() - acc += check_sizeof_absent() acc += check_exception_group_immutable() - acc += check_getsizeof() i += 1 print(acc) diff --git a/pyre/extra_tests/parity_tests/blockingioerror_characters_written.py b/pyre/extra_tests/parity_tests/blockingioerror_characters_written.py index 9e3e95e909a..205df51d1e5 100644 --- a/pyre/extra_tests/parity_tests/blockingioerror_characters_written.py +++ b/pyre/extra_tests/parity_tests/blockingioerror_characters_written.py @@ -26,4 +26,11 @@ assert plain.characters_written == 7 del plain.characters_written +generic = Exception() +assert not hasattr(generic, "characters_written") +generic.characters_written = 11 +assert generic.__dict__["characters_written"] == 11 +del generic.characters_written +assert not hasattr(generic, "characters_written") + print("OK") diff --git a/pyre/extra_tests/parity_tests/re_jit_call_resume.py b/pyre/extra_tests/parity_tests/re_jit_call_resume.py new file mode 100644 index 00000000000..cdbce14235a --- /dev/null +++ b/pyre/extra_tests/parity_tests/re_jit_call_resume.py @@ -0,0 +1,7 @@ +import re + + +for i in range(10_000): + re.compile(str(i) + "|x") + +print("OK") diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index 03b80e65352..0f7bf8f1b9a 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -436,21 +436,13 @@ pub fn exception_getclass(w_obj: PyObjectRef) -> PyObjectRef { } /// True when `obj` is a `BlockingIOError` whose constructor took the numeric -/// third argument as `characters_written` — recognised by `args_w[2]` still -/// being an int (every other 2..=5-argument form trims `args_w` to two -/// elements). Suppresses `filename` derivation for that constructor argument -/// even after the independent `written` slot is later deleted -/// (`interp_exceptions.py` `_init_error`). +/// third argument as `characters_written`. The constructor records the +/// successful `__index__` conversion independently from `args_w` because the +/// original, possibly non-int indexable object remains in `args`, and from the +/// writable/deletable `written` slot. This suppresses `filename` even after +/// the slot is deleted (`interp_exceptions.py` `_init_error`). fn exc_blocking_written(obj: PyObjectRef) -> bool { - let args = unsafe { pyre_object::interp_exceptions::w_exception_get_args(obj) }; - let n = unsafe { pyre_object::w_tuple_len(args) }; - if n < 3 { - return false; - } - let Some(v) = (unsafe { pyre_object::w_tuple_getitem(args, 2) }) else { - return false; - }; - if !unsafe { pyre_object::is_int(v) } { + if !unsafe { pyre_object::interp_exceptions::w_exception_get_blocking_written_arg(obj) } { return false; } let Some(blocking) = crate::builtins::lookup_exc_class("BlockingIOError") else { diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index 97e10933833..aed06bdcfb4 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -6254,6 +6254,7 @@ fn os_error_fill_slots(exc: PyObjectRef, args: &[PyObjectRef]) { }; if let Some(value) = w_filename.and_then(written) { interp_exceptions::w_exception_set_written(exc, value); + interp_exceptions::w_exception_set_blocking_written_arg(exc); } else if let Some(fname) = w_filename { interp_exceptions::w_exception_set_filename(exc, fname); if let Some(f2) = args.get(4).copied().filter(|&f| !pyre_object::is_none(f)) { @@ -13311,6 +13312,9 @@ pub(crate) fn sort_list_in_place( sort_scalars(std::slice::from_raw_parts_mut(items, len), reverse)?; return Ok(()); } + if pyre_object::listobject::w_list_sort_int_or_float(list, reverse) { + return Ok(()); + } } } unsafe { diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index 0b8a2d5f854..df563971580 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -5147,15 +5147,14 @@ pub unsafe fn create_all_slots( }; // CPython 3.14 `type_new_slots`: a variable-sized base may add a // managed instance dict, but may not add weakrefs or any explicit - // `__slots__` entry. These are the three variable layouts currently - // exposed by pyre's builtin type registry. + // `__slots__` entry. Derive this from the same layout metadata which + // exposes `tp_itemsize`, so newly ported variable builtins cannot be + // omitted from type creation semantics. let base_has_variable_items = if base_layout.is_null() { false } else { - let typedef = (*base_layout).typedef; - std::ptr::eq(typedef, &pyre_object::INT_TYPE) - || std::ptr::eq(typedef, &pyre_object::TUPLE_TYPE) - || std::ptr::eq(typedef, &pyre_object::bytesobject::BYTES_TYPE) + crate::typedef::cpython_type_layout(w_bestbase) + .is_some_and(|(_, itemsize)| itemsize != 0) }; // typeobject.py:1150-1204 create_all_slots diff --git a/pyre/pyre-interpreter/src/function.rs b/pyre/pyre-interpreter/src/function.rs index aba55d35ac7..c8cb90910e3 100644 --- a/pyre/pyre-interpreter/src/function.rs +++ b/pyre/pyre-interpreter/src/function.rs @@ -520,19 +520,23 @@ pub(crate) fn function_new_impl( PY_NULL } else { let globals_backing = crate::type_methods::resolve_dict_backing(w_func_globals_obj); - let selected = unsafe { pyre_object::w_dict_getitem_str(globals_backing, "__builtins__") } - .unwrap_or_else(|| { - let exec_ctx = crate::call::take_last_exec_ctx(); - if exec_ctx.is_null() { - return PY_NULL; - } - let frame = unsafe { (*exec_ctx).gettopframe_raw() }; - if frame.is_null() { - unsafe { (*exec_ctx).get_builtin_dict() } - } else { - unsafe { (*frame).fget_f_builtins() } - } - }); + let selected = if globals_backing.is_null() { + None + } else { + unsafe { pyre_object::w_dict_getitem_str(globals_backing, "__builtins__") } + } + .unwrap_or_else(|| { + let exec_ctx = crate::call::take_last_exec_ctx(); + if exec_ctx.is_null() { + return PY_NULL; + } + let frame = unsafe { (*exec_ctx).gettopframe_raw() }; + if frame.is_null() { + unsafe { (*exec_ctx).get_builtin_dict() } + } else { + unsafe { (*frame).fget_f_builtins() } + } + }); if !selected.is_null() && unsafe { pyre_object::is_module(selected) } { unsafe { pyre_object::w_module_get_w_dict(selected) } } else { diff --git a/pyre/pyre-interpreter/src/module/sys/vm.rs b/pyre/pyre-interpreter/src/module/sys/vm.rs index b06f1edb5fa..e14203f4082 100644 --- a/pyre/pyre-interpreter/src/module/sys/vm.rs +++ b/pyre/pyre-interpreter/src/module/sys/vm.rs @@ -54,13 +54,28 @@ fn get_sizeof(w_obj: PyObjectRef) -> crate::PyResult { )); } - // `_PyType_PreHeaderSize(Py_TYPE(o))`: on 64-bit CPython a managed heap - // instance has a 16-byte GC header plus a 16-byte managed dict/weakref - // prefix (half those sizes on 32-bit). This unit covers heap instances and - // untracked str; tracked builtin types will extend the type-layout port. - let pre_header = crate::typedef::r#type(current()) - .filter(|tp| unsafe { pyre_object::w_type_is_heaptype(tp.as_ptr()) }) - .map_or(0u64, |_| (4 * std::mem::size_of::()) as u64); + // `_PyType_PreHeaderSize(Py_TYPE(o))` adds its two components + // independently: a two-word GC header for tracked objects, plus a two-word + // managed dict/weakref prefix where the instance type requests it. A + // tracked builtin such as list has only the first; a normal heap instance + // has both; an untracked heap-derived value may have only the second. + let word = std::mem::size_of::() as u64; + let gc_header = if pyre_object::gc_hook::try_gc_owns_object(current() as *mut u8) { + 2 * word + } else { + 0 + }; + let managed_prefix = crate::typedef::r#type(current()).map_or(0, |tp| unsafe { + if pyre_object::w_type_is_heaptype(tp.as_ptr()) + && (pyre_object::w_type_get_hasdict(tp.as_ptr()) + || pyre_object::w_type_get_weakrefable(tp.as_ptr())) + { + 2 * word + } else { + 0 + } + }); + let pre_header = gc_header + managed_prefix; let total = (size as u64) .checked_add(pre_header) .expect("Py_ssize_t plus the fixed pre-header fits in size_t"); diff --git a/pyre/pyre-interpreter/src/pyframe.rs b/pyre/pyre-interpreter/src/pyframe.rs index d4f089e6442..de98a529fb2 100644 --- a/pyre/pyre-interpreter/src/pyframe.rs +++ b/pyre/pyre-interpreter/src/pyframe.rs @@ -3291,13 +3291,26 @@ impl PyFrame { "cannot clear a suspended frame", )); } - crate::baseobjspace::generator_finalize(w_gen)?; + // `_PyGen_Finalize` can execute Python and collect. Keep both + // owner and frame as explicit roots, then reload both before + // severing the association; neither raw pointer may survive + // the call unchanged across a nursery move. + let frame_anchor = crate::eval::FrameAnchor::new(self); + let roots = pyre_object::gc_roots::push_roots(); + let gen_slot = roots.base(); + roots.pin_root(w_gen); + crate::baseobjspace::generator_finalize(roots.get(gen_slot))?; // CPython 3.14 `frame_clear_impl` finishes a never-started // generator after `_PyGen_Finalize`, then clears the owned // interpreter frame. The suspended case was rejected above, // so this is the same completion path used by close() and // normal generator return, including severing `gi_frame`. - unsafe { crate::baseobjspace::generator_frame_is_finished(w_gen, self) }; + unsafe { + crate::baseobjspace::generator_frame_is_finished( + roots.get(gen_slot), + &mut *frame_anchor.live(), + ) + }; let ec = crate::call::getexecutioncontext() as *mut crate::executioncontext::ExecutionContext; if !ec.is_null() { diff --git a/pyre/pyre-interpreter/src/type_methods.rs b/pyre/pyre-interpreter/src/type_methods.rs index 67f2294d372..df58b8f385f 100644 --- a/pyre/pyre-interpreter/src/type_methods.rs +++ b/pyre/pyre-interpreter/src/type_methods.rs @@ -589,7 +589,10 @@ pub fn list_method_extend(args: &[PyObjectRef]) -> Result Option<(i64, i64)> { +pub(crate) fn cpython_type_layout(w_type: PyObjectRef) -> Option<(i64, i64)> { if w_type.is_null() || !unsafe { pyre_object::is_type(w_type) } { return None; } @@ -10341,7 +10341,10 @@ fn cpython_type_layout(w_type: PyObjectRef) -> Option<(i64, i64)> { // subclasses append their declared slots to this prefix. (8 * word, 0) } else { - return None; + // CPython's ordinary fixed-size heap instance begins with + // PyObject_HEAD; user slots are appended below just like the + // specialized builtin prefixes above. + (2 * word, 0) }; // PyPy typeobject.py:103-129 keeps the total slot count on Layout, whose // typedef identifies the fixed builtin prefix. CPython appends one pointer @@ -10432,6 +10435,35 @@ fn init_type_type(ns: PyObjectRef) { make_new_descr(crate::builtins::type_descr_new), ) }; + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__sizeof__", + crate::gateway::make_builtin_function_with_arity_and_text_signature( + "__sizeof__", + |args| { + crate::type_methods::arity_no_args(args, "__sizeof__")?; + let word = std::mem::size_of::() as i64; + let size = if pyre_object::w_type_is_heaptype(args[0]) { + // CPython 3.14 typeobject.c:type___sizeof___impl: + // PyHeapTypeObject plus the cached-keys table carried + // by a managed instance dictionary. + 117 * word + + if pyre_object::w_type_get_hasdict(args[0]) { + 96 * word + } else { + 0 + } + } else { + 52 * word + }; + Ok(w_int_new(size)) + }, + 1, + "($self, /)", + ), + ) + }; // `type[int]` builds a GenericAlias, but `type` carries no // `__class_getitem__` in its dict — `descroperation.getitem` special-cases // `is_w(w_obj, w_type)` (`descroperation.py:362`). The wiring lives in @@ -19085,8 +19117,25 @@ fn init_object_type(ns: PyObjectRef) { .unwrap_or((2 * std::mem::size_of::() as i64, 0)); let nitems = if itemsize == 0 { 0 - } else { + } else if unsafe { + pyre_object::is_bool(args[0]) + || pyre_object::is_int(args[0]) + || pyre_object::is_long(args[0]) + } { int_cpython_digit_count(args[0])? + } else if unsafe { pyre_object::is_tuple(args[0]) } { + unsafe { pyre_object::w_tuple_len(args[0]) as i64 } + } else if unsafe { pyre_object::is_bytes(args[0]) } { + unsafe { pyre_object::w_bytes_len(args[0]) as i64 } + } else if std::ptr::eq( + unsafe { pyre_object::w_type_get_layout(w_type) }, + &pyre_object::memoryview::MEMORYVIEW_TYPE, + ) { + // PyMemoryViewObject's variable tail stores three + // Py_ssize_t arrays (shape, strides, suboffsets). + 3 * unsafe { pyre_object::memoryview::w_memoryview_ndim(args[0]) } + } else { + 0 }; Ok(w_int_new(basicsize + itemsize * nitems)) }, diff --git a/pyre/pyre-jit-trace/src/descr.rs b/pyre/pyre-jit-trace/src/descr.rs index 0e458ed025f..1c99d1d51dd 100644 --- a/pyre/pyre-jit-trace/src/descr.rs +++ b/pyre/pyre-jit-trace/src/descr.rs @@ -1510,6 +1510,20 @@ static W_LIST_DESCR_GROUP: LazyLock = LazyLock::new(|| { false, false, ), + // CPython 3.14 `PyListObject.allocated`. The orthodox append + // descent now walks `W_ListObject::sync_allocated`, so this field + // must belong to the canonical list descr group just like every + // other mutable field reached by the translated body. Keep it at + // the end so the established list field indices remain stable. + ( + "allocated", + std::mem::offset_of!(W_ListObject, allocated), + std::mem::size_of::(), + Type::Int, + false, + false, + false, + ), ], "W_ListObject", "listobject::W_ListObject", diff --git a/pyre/pyre-jit-trace/src/helpers.rs b/pyre/pyre-jit-trace/src/helpers.rs index 20fc5d901c0..e7f1f7323e0 100644 --- a/pyre/pyre-jit-trace/src/helpers.rs +++ b/pyre/pyre-jit-trace/src/helpers.rs @@ -1229,8 +1229,15 @@ pub fn emit_promote_empty_list_inline( // capacity through `list.items` (list_items_descr), a path that // already resolves to the concrete block. } - pyre_object::listobject::ListStrategy::Empty => { - debug_assert_ne!(strategy, pyre_object::listobject::ListStrategy::Empty); + pyre_object::listobject::ListStrategy::Empty + | pyre_object::listobject::ListStrategy::IntOrFloat => { + // First append can only select Integer, Float, or Object; + // IntOrFloat is reached later by a numeric strategy transition. + debug_assert!(matches!( + strategy, + pyre_object::listobject::ListStrategy::Empty + | pyre_object::listobject::ListStrategy::IntOrFloat + )); } } } diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs index 52930c24519..bf3c8d390aa 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs @@ -1016,6 +1016,21 @@ pub(crate) fn fbw_store_journal_rollback() { w_item, ); } + pyre_object::listobject::ListStrategy::IntOrFloat => { + pyre_object::listobject::w_list_int_or_float_set_len( + list, + length_before, + ); + if !pyre_object::listobject::w_list_int_or_float_setitem( + list, + length_before - 1, + w_item, + ) { + crate::trace::fbw_diag::bump( + crate::trace::fbw_diag::STORE_JOURNAL_ROLLBACK_FAILED, + ); + } + } pyre_object::listobject::ListStrategy::Float | pyre_object::listobject::ListStrategy::Empty => { crate::trace::fbw_diag::bump( @@ -1049,6 +1064,12 @@ pub(crate) fn fbw_store_journal_rollback() { pyre_object::listobject::ListStrategy::Integer => { pyre_object::listobject::ll_list_int_set_len(list_ref, length_before); } + pyre_object::listobject::ListStrategy::IntOrFloat => { + pyre_object::listobject::w_list_int_or_float_set_len( + list, + length_before, + ); + } // Float items are non-ptr f64 scalars (no stale GC ref to // clear, unlike the Object slot), so rewinding the length // field suffices. diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs index dde83b53758..2c02635bb04 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs @@ -4146,6 +4146,11 @@ pub(crate) fn try_walker_specialize_newlist( Emit::Float(vals) } ListStrategy::Object => Emit::Object, + // The interpreter stores this as encoded signed-longlong values. + // The walker does not yet have an encoded numeric payload variant; + // leave construction to the ordinary residual instead of emitting an + // Integer array whose values would have the wrong representation. + ListStrategy::IntOrFloat => return Ok(None), // Empty is impossible here (len >= 1); decline defensively. ListStrategy::Empty => return Ok(None), }; diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs index ba817110c41..cc64bb0a14a 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs @@ -85,28 +85,6 @@ fn after_residual_guard_uses_trailing_live_before_fallthrough_twin() { ); } -#[test] -fn method_load_global_snapshot_preserves_live_null_call_slot() { - let mut tc = TraceCtx::for_test_types(&[Type::Ref]); - let kept = tc.const_ref(41); - let stale = tc.const_ref(42); - let null = tc.const_null(); - let mut boxes = vec![kept, stale, stale]; - - super::vstack_mirror::reconcile_load_global_method_shape(&mut boxes, 1, 3, null); - - assert_eq!(boxes, vec![kept, OpRef::NONE, null]); - assert_eq!( - super::resume_snapshot::vstack_box_for_snapshot(null), - Some(null), - "CALL's NULL self slot must clear a stale vable shadow value", - ); - assert_eq!( - super::resume_snapshot::vstack_box_for_snapshot(OpRef::NONE), - None, - ); -} - #[test] fn vstack_permuted_for_iter_entry_uses_block_head_target() { let mut pyjit = crate::PyJitCode::skeleton(std::ptr::null()); @@ -2362,7 +2340,9 @@ fn append_journal_rollback_rewinds_length() { #[test] fn pop_end_journal_rollback_restores_item_and_length() { - use pyre_object::listobject::{W_ListObject, ll_list_int_getitem_fast, w_list_len, w_list_new}; + use pyre_object::listobject::{ + W_ListObject, ll_list_int_getitem_fast, w_list_allocated, w_list_len, w_list_new, + }; use pyre_object::{w_int_new, w_list_pop_end}; super::fbw_store_journal_reset(); @@ -2416,7 +2396,8 @@ fn interleaved_append_pop_journal_rollback_restores_original() { let list = w_list_new(original.into_iter().map(w_int_new).collect()); let len_before_append = unsafe { w_list_len(list) }; - super::fbw_list_journal_push_append(list, len_before_append); + let allocated_before_append = unsafe { w_list_allocated(list) }; + super::fbw_list_journal_push_append(list, len_before_append, allocated_before_append); unsafe { w_list_append(list, w_int_new(40)) }; let len_before_pop = unsafe { w_list_len(list) }; @@ -2426,7 +2407,8 @@ fn interleaved_append_pop_journal_rollback_restores_original() { super::fbw_list_journal_push_pop_end(list, len_before_pop, w_int_new(raw_item)); let len_before_append = unsafe { w_list_len(list) }; - super::fbw_list_journal_push_append(list, len_before_append); + let allocated_before_append = unsafe { w_list_allocated(list) }; + super::fbw_list_journal_push_append(list, len_before_append, allocated_before_append); unsafe { w_list_append(list, w_int_new(50)) }; let len_before_pop = unsafe { w_list_len(list) }; diff --git a/pyre/pyre-object/src/float_array.rs b/pyre/pyre-object/src/float_array.rs index cc80b3f6a50..d442c0e2a4c 100644 --- a/pyre/pyre-object/src/float_array.rs +++ b/pyre/pyre-object/src/float_array.rs @@ -21,7 +21,7 @@ pub struct FloatArray { /// `Ptr(GcArray(Float))` — the backing block (`l.items`). Always non-null. pub block: *mut TypedItemsBlock, /// Live length (rlist.py:116 `("length", Signed)`). - pub(crate) len: usize, + len: usize, } pub const FLOAT_ARRAY_BLOCK_OFFSET: usize = std::mem::offset_of!(FloatArray, block); diff --git a/pyre/pyre-object/src/int_array.rs b/pyre/pyre-object/src/int_array.rs index 140efdfc5a1..5aa0f57d766 100644 --- a/pyre/pyre-object/src/int_array.rs +++ b/pyre/pyre-object/src/int_array.rs @@ -25,7 +25,7 @@ pub struct IntArray { /// `Ptr(GcArray(Signed))` — the backing block (`l.items`). Always non-null. pub block: *mut TypedItemsBlock, /// Live length (rlist.py:116 `("length", Signed)`). - pub(crate) len: usize, + len: usize, } pub const INT_ARRAY_BLOCK_OFFSET: usize = std::mem::offset_of!(IntArray, block); diff --git a/pyre/pyre-object/src/interp_exceptions.rs b/pyre/pyre-object/src/interp_exceptions.rs index 3c9027868b1..b8fb5d362d9 100644 --- a/pyre/pyre-object/src/interp_exceptions.rs +++ b/pyre/pyre-object/src/interp_exceptions.rs @@ -387,6 +387,11 @@ pub struct W_BaseException { /// A numeric third argument on an exact BlockingIOError stamps it; later /// descriptor writes and deletes mutate this slot without changing args. pub written: i64, + /// Whether the exact BlockingIOError constructor successfully interpreted + /// its third argument through `__index__` as `characters_written`. This + /// is constructor shape, independent of later writes/deletion of + /// `written`; CPython continues to suppress `filename` after deletion. + pub blocking_written_arg: bool, /// `interp_exceptions.py:990 W_SystemExit.w_code` / /// `:1006 readwrite_attrproperty_w('w_code', W_SystemExit)`. /// `PY_NULL` is the class default `None`; the `code` getattr arm @@ -706,6 +711,7 @@ fn w_exception_new_empty_impl(kind: ExcKind, immortal: bool) -> PyObjectRef { w_filename2: PY_NULL, // `interp_exceptions.py:528` W_OSError class default. written: -1, + blocking_written_arg: false, // `interp_exceptions.py:990` W_SystemExit class default // `w_code = None`. w_code: PY_NULL, @@ -1390,6 +1396,16 @@ pub unsafe fn w_exception_set_written(obj: PyObjectRef, value: i64) { unsafe { (*(obj as *mut W_BaseException)).written = value }; } +#[inline] +pub unsafe fn w_exception_get_blocking_written_arg(obj: PyObjectRef) -> bool { + unsafe { (*(obj as *const W_BaseException)).blocking_written_arg } +} + +#[inline] +pub unsafe fn w_exception_set_blocking_written_arg(obj: PyObjectRef) { + unsafe { (*(obj as *mut W_BaseException)).blocking_written_arg = true }; +} + /// `interp_exceptions.py:1006 readwrite_attrproperty_w('w_code', ...)` /// — `e.code` reader. `PY_NULL` means the slot was never written (the /// `code` getattr arm then derives the value from `args_w`). diff --git a/pyre/pyre-object/src/listobject.rs b/pyre/pyre-object/src/listobject.rs index 7015296fda3..ce068bb66b6 100644 --- a/pyre/pyre-object/src/listobject.rs +++ b/pyre/pyre-object/src/listobject.rs @@ -85,6 +85,10 @@ pub enum ListStrategy { /// without any storage yet. First append picks a typed strategy via /// switch_to_correct_strategy. Empty = 3, + /// listobject.py:2193 IntOrFloatListStrategy. Entries share the + /// `int_items` signed-longlong array: int32 values use RPython's + /// 0xfffffffe NaN payload and floats keep their raw IEEE-754 bits. + IntOrFloat = 4, } /// Python list object. @@ -96,11 +100,10 @@ pub enum ListStrategy { /// offset-0 header holds the allocated capacity /// (upstream `len(l.items)` per rlist.py:251). /// -/// `strategy`, `int_items`, `float_items` are pyre-only -/// TODOs for PyPy's list strategy split -/// (`pypy/objspace/std/listobject.py`). Only the Object strategy -/// reads/writes `length` + `items`; Integer/Float strategies operate -/// on their own typed arrays and keep `length = 0`, `items = null`. +/// `strategy`, `int_items`, `float_items` implement PyPy's list strategy split +/// (`pypy/objspace/std/listobject.py`). Only the Object strategy reads/writes +/// `length` + `items`; Integer/IntOrFloat/Float strategies operate on their +/// own typed arrays and keep `length = 0`, `items = null`. #[repr(C)] pub struct W_ListObject { pub ob_header: PyObject, @@ -119,7 +122,7 @@ pub struct W_ListObject { /// the `ItemsBlock` whose offset-0 header is the allocated /// capacity (= upstream `len(l.items)` per rlist.py:251). Null /// when the list is in a non-Object strategy (Empty/Integer/ - /// Float); lazily allocated on strategy switch. + /// IntOrFloat/Float); lazily allocated on strategy switch. pub items: *mut ItemsBlock, pub strategy: ListStrategy, pub int_items: IntArray, @@ -150,8 +153,9 @@ impl W_ListObject { // Direct rlist `length` field reads keep this helper in the // annotator's structural subset; the public `.len()` wrappers are // host collection conveniences that translate as `__len__`. - ListStrategy::Integer => self.int_items.len, - ListStrategy::Float => self.float_items.len, + ListStrategy::Integer => self.int_items.len(), + ListStrategy::IntOrFloat => self.int_items.len(), + ListStrategy::Float => self.float_items.len(), } } @@ -546,6 +550,108 @@ fn all_floats(items: &[PyObjectRef]) -> bool { .all(|&item| unsafe { is_float_strategy_item(item) }) } +// rpython/rlib/longlong2float.py:90-150. Keep these bit operations local to +// IntOrFloatListStrategy: the signed-longlong storage representation is part +// of the upstream strategy, not a general numeric coercion. +const INT_OR_FLOAT_INT_HIGH_WORD: u32 = 0xffff_fffe; + +#[inline] +fn int_or_float_is_int(value: i64) -> bool { + ((value as u64) >> 32) as u32 == INT_OR_FLOAT_INT_HIGH_WORD +} + +#[inline] +fn int_or_float_encode_int(value: i64) -> Option { + let value = i32::try_from(value).ok()?; + Some(((INT_OR_FLOAT_INT_HIGH_WORD as u64) << 32 | value as u32 as u64) as i64) +} + +#[inline] +fn int_or_float_encode_float(value: f64) -> Option { + let bits = value.to_bits(); + (((bits >> 32) as u32) != INT_OR_FLOAT_INT_HIGH_WORD).then_some(bits as i64) +} + +#[inline] +fn int_or_float_decode_int(value: i64) -> i64 { + value as u32 as i32 as i64 +} + +#[inline] +fn int_or_float_as_float(value: i64) -> f64 { + if int_or_float_is_int(value) { + int_or_float_decode_int(value) as f64 + } else { + f64::from_bits(value as u64) + } +} + +#[inline] +unsafe fn int_or_float_encode_item(item: PyObjectRef) -> Option { + if is_plain_int1(item) { + int_or_float_encode_int(plain_int_w(item)) + } else if is_float_strategy_item(item) { + int_or_float_encode_float(w_float_get_value(item)) + } else { + None + } +} + +fn all_int_or_float(items: &[PyObjectRef]) -> bool { + items + .iter() + .all(|&item| unsafe { int_or_float_encode_item(item).is_some() }) +} + +fn boxed_from_int_or_float(values: &[i64]) -> Vec { + let _roots = crate::gc_roots::push_roots(); + let root_base = crate::gc_roots::shadow_stack_len(); + for &value in values { + let item = if int_or_float_is_int(value) { + w_int_new(int_or_float_decode_int(value)) + } else { + w_float_new(f64::from_bits(value as u64)) + }; + crate::gc_roots::pin_root(item); + } + (0..values.len()) + .map(|i| crate::gc_roots::shadow_stack_get(root_base + i)) + .collect() +} + +/// listobject.py:2034 IntegerListStrategy.switch_to_int_or_float_strategy. +unsafe fn integer_to_int_or_float(list: &mut W_ListObject) -> bool { + let Some(values) = list + .int_items + .as_slice() + .iter() + .map(|&value| int_or_float_encode_int(value)) + .collect::>>() + else { + return false; + }; + list.int_items.install(IntArray::from_vec(values)); + list.strategy = ListStrategy::IntOrFloat; + true +} + +/// listobject.py:2156 FloatListStrategy.switch_to_int_or_float_strategy. +unsafe fn float_to_int_or_float(list: &mut W_ListObject) -> bool { + let Some(values) = list + .float_items + .as_slice() + .iter() + .map(|&value| int_or_float_encode_float(value)) + .collect::>>() + else { + return false; + }; + list.int_items.install(IntArray::from_vec(values)); + list.float_items.install(FloatArray::from_vec(Vec::new())); + list.strategy = ListStrategy::IntOrFloat; + true +} + fn boxed_from_ints(values: &[i64]) -> Vec { let _roots = crate::gc_roots::push_roots(); let root_base = crate::gc_roots::shadow_stack_len(); @@ -586,6 +692,7 @@ pub unsafe fn switch_to_object_strategy(list: &mut W_ListObject) { } let seed: Vec = match list.strategy { ListStrategy::Integer => boxed_from_ints(list.int_items.as_slice()), + ListStrategy::IntOrFloat => boxed_from_int_or_float(list.int_items.as_slice()), ListStrategy::Float => boxed_from_floats(list.float_items.as_slice()), ListStrategy::Object | ListStrategy::Empty => Vec::new(), }; @@ -624,6 +731,8 @@ pub fn list_strategy_for(items: &[PyObjectRef]) -> ListStrategy { ListStrategy::Integer } else if all_floats(items) { ListStrategy::Float + } else if all_int_or_float(items) { + ListStrategy::IntOrFloat } else { ListStrategy::Object } @@ -760,10 +869,13 @@ pub fn w_list_new_empty() -> PyObjectRef { /// scope those pins live in and closes the bracket with /// [`ListStorage::reload_typed_blocks`] after its last allocation. unsafe fn build_list_storage(items: &[PyObjectRef], strategy: ListStrategy) -> ListStorage { - let int_seed: Vec = if let ListStrategy::Integer = strategy { - items.iter().map(|&item| plain_int_w(item)).collect() - } else { - Vec::new() + let int_seed: Vec = match strategy { + ListStrategy::Integer => items.iter().map(|&item| plain_int_w(item)).collect(), + ListStrategy::IntOrFloat => items + .iter() + .map(|&item| int_or_float_encode_item(item).unwrap()) + .collect(), + _ => Vec::new(), }; let int_items = IntArray::from_vec(int_seed); let int_block_root = int_items.pin_block(); @@ -1117,6 +1229,20 @@ pub unsafe fn w_list_getitem(obj: PyObjectRef, index: i64) -> Option { + let items = list.int_items.as_slice(); + let len = items.len() as i64; + let idx = if index < 0 { index + len } else { index }; + if idx < 0 || idx >= len { + return None; + } + let value = items[idx as usize]; + Some(if int_or_float_is_int(value) { + w_int_new(int_or_float_decode_int(value)) + } else { + w_float_new(f64::from_bits(value as u64)) + }) + } ListStrategy::Float => { let items = list.float_items.as_slice(); let len = items.len() as i64; @@ -1169,6 +1295,22 @@ pub unsafe fn w_list_setitem(obj: PyObjectRef, index: i64, value: PyObjectRef) - if is_plain_int1(value) { ll_list_int_setitem_fast(list, idx as usize, plain_int_w(value)); true + } else if is_float_strategy_item(value) && integer_to_int_or_float(list) { + w_list_setitem(obj, index, value) + } else { + switch_to_object_strategy(list); + w_list_setitem(obj, index, value) + } + } + ListStrategy::IntOrFloat => { + let len = list.int_items.len() as i64; + let idx = if index < 0 { index + len } else { index }; + if idx < 0 || idx >= len { + return false; + } + if let Some(value) = int_or_float_encode_item(value) { + list.int_items[idx as usize] = value; + true } else { switch_to_object_strategy(list); w_list_setitem(obj, index, value) @@ -1183,6 +1325,11 @@ pub unsafe fn w_list_setitem(obj: PyObjectRef, index: i64, value: PyObjectRef) - if is_float_strategy_item(value) { list.float_items[idx as usize] = w_float_get_value(value); true + } else if is_plain_int1(value) + && int_or_float_encode_int(plain_int_w(value)).is_some() + && float_to_int_or_float(list) + { + w_list_setitem(obj, index, value) } else { switch_to_object_strategy(list); w_list_setitem(obj, index, value) @@ -1303,6 +1450,8 @@ pub unsafe fn w_list_append_inner(obj: PyObjectRef, value: PyObjectRef) { } else { list.int_items.push(item); } + } else if is_float_strategy_item(value) && integer_to_int_or_float(list) { + w_list_append_inner(obj, value); } else { switch_to_object_strategy(list); list.object_push(value); @@ -1329,6 +1478,19 @@ pub unsafe fn w_list_append_inner(obj: PyObjectRef, value: PyObjectRef) { } else { list.float_items.push(item); } + } else if is_plain_int1(value) + && int_or_float_encode_int(plain_int_w(value)).is_some() + && float_to_int_or_float(list) + { + w_list_append_inner(obj, value); + } else { + switch_to_object_strategy(list); + list.object_push(value); + } + } + ListStrategy::IntOrFloat => { + if let Some(item) = int_or_float_encode_item(value) { + list.int_items.push(item); } else { switch_to_object_strategy(list); list.object_push(value); @@ -1386,6 +1548,29 @@ pub unsafe fn w_list_int_set_len(obj: PyObjectRef, n: usize) { ll_list_int_set_len(list, n); } +/// JIT rollback leaves for IntOrFloatListStrategy's signed-longlong storage. +/// These mirror the Integer leaves but encode the restored boxed value using +/// listobject.py:2193 `IntOrFloatListStrategy.unwrap`. +pub unsafe fn w_list_int_or_float_set_len(obj: PyObjectRef, n: usize) { + let list = &mut *(obj as *mut W_ListObject); + debug_assert_eq!(list.strategy, ListStrategy::IntOrFloat); + list.int_items.set_len(n); +} + +pub unsafe fn w_list_int_or_float_setitem( + obj: PyObjectRef, + index: usize, + value: PyObjectRef, +) -> bool { + let list = &mut *(obj as *mut W_ListObject); + debug_assert_eq!(list.strategy, ListStrategy::IntOrFloat); + let Some(value) = int_or_float_encode_item(value) else { + return false; + }; + list.int_items[index] = value; + true +} + /// Get the length of a list. /// /// # Safety @@ -1403,6 +1588,7 @@ pub unsafe fn w_list_len(obj: PyObjectRef) -> usize { ListStrategy::Empty => 0, ListStrategy::Object => list.length, ListStrategy::Integer => ll_list_int_length(list), + ListStrategy::IntOrFloat => list.int_items.len(), ListStrategy::Float => list.float_items.len(), } } @@ -1495,6 +1681,7 @@ pub unsafe fn w_list_can_append_without_realloc(obj: PyObjectRef) -> bool { ListStrategy::Empty => false, ListStrategy::Object => list.object_spare_capacity() > 0, ListStrategy::Integer => list.int_items.spare_capacity() > 0, + ListStrategy::IntOrFloat => list.int_items.spare_capacity() > 0, ListStrategy::Float => list.float_items.spare_capacity() > 0, } } @@ -1513,6 +1700,7 @@ pub unsafe fn w_list_is_inline_storage(obj: PyObjectRef) -> bool { // "inline" bit either. ListStrategy::Object => false, ListStrategy::Integer => list.int_items.is_inline(), + ListStrategy::IntOrFloat => list.int_items.is_inline(), ListStrategy::Float => list.float_items.is_inline(), } } @@ -1532,6 +1720,11 @@ pub unsafe fn w_list_uses_float_storage(obj: PyObjectRef) -> bool { list.strategy == ListStrategy::Float } +pub unsafe fn w_list_uses_int_or_float_storage(obj: PyObjectRef) -> bool { + let list = &*(obj as *const W_ListObject); + list.strategy == ListStrategy::IntOrFloat +} + pub unsafe fn w_list_uses_empty_storage(obj: PyObjectRef) -> bool { let list = &*(obj as *const W_ListObject); list.strategy == ListStrategy::Empty @@ -1618,6 +1811,7 @@ unsafe fn temporarily_as_objects(list: &W_ListObject) -> Vec { .map(|i| crate::gc_roots::shadow_stack_get(root_base + i)) .collect() } + ListStrategy::IntOrFloat => boxed_from_int_or_float(list.int_items.as_slice()), ListStrategy::Float => { let items = list.float_items.as_slice(); let _roots = crate::gc_roots::push_roots(); @@ -1662,6 +1856,20 @@ pub unsafe fn w_list_insert(obj: PyObjectRef, index: i64, value: PyObjectRef) { list.sync_allocated(old_size); return; } + if is_float_strategy_item(value) && integer_to_int_or_float(list) { + w_list_insert(obj, index, value); + } else { + switch_to_object_strategy(list); + w_list_insert(obj, index, value); + } + } + ListStrategy::IntOrFloat => { + if let Some(value) = int_or_float_encode_item(value) { + let idx = normalize_insert_index(index, list.int_items.len()); + list.int_items.insert(idx, value); + list.sync_allocated(old_size); + return; + } switch_to_object_strategy(list); w_list_insert(obj, index, value); } @@ -1672,8 +1880,15 @@ pub unsafe fn w_list_insert(obj: PyObjectRef, index: i64, value: PyObjectRef) { list.sync_allocated(old_size); return; } - switch_to_object_strategy(list); - w_list_insert(obj, index, value); + if is_plain_int1(value) + && int_or_float_encode_int(plain_int_w(value)).is_some() + && float_to_int_or_float(list) + { + w_list_insert(obj, index, value); + } else { + switch_to_object_strategy(list); + w_list_insert(obj, index, value); + } } ListStrategy::Object => { let idx = normalize_insert_index(index, list.length); @@ -1710,6 +1925,22 @@ pub unsafe fn w_list_pop(obj: PyObjectRef, index: i64) -> Option { let item = list.int_items.remove(idx as usize); Some(w_int_new(item)) } + ListStrategy::IntOrFloat => { + let len = list.int_items.len() as i64; + if len == 0 { + return None; + } + let idx = if index < 0 { index + len } else { index }; + if idx < 0 || idx >= len { + return None; + } + let item = list.int_items.remove(idx as usize); + Some(if int_or_float_is_int(item) { + w_int_new(int_or_float_decode_int(item)) + } else { + w_float_new(f64::from_bits(item as u64)) + }) + } ListStrategy::Float => { let len = list.float_items.len() as i64; if len == 0 { @@ -1767,6 +1998,7 @@ pub unsafe fn w_list_pop_end(obj: PyObjectRef) -> Option { let length = match list.strategy { ListStrategy::Empty => 0, ListStrategy::Integer => ll_list_int_length(list), + ListStrategy::IntOrFloat => list.int_items.len(), ListStrategy::Float => list.float_items.len(), ListStrategy::Object => list.length, }; @@ -1800,6 +2032,14 @@ pub unsafe fn w_list_pop_end_inner(obj: PyObjectRef) -> PyObjectRef { ll_list_int_set_len(list, index); w_int_new(item) } + ListStrategy::IntOrFloat => { + let item = list.int_items.pop(); + if int_or_float_is_int(item) { + w_int_new(int_or_float_decode_int(item)) + } else { + w_float_new(f64::from_bits(item as u64)) + } + } ListStrategy::Float => w_float_new(list.float_items.pop()), ListStrategy::Object => list.object_pop(), } @@ -1837,6 +2077,31 @@ pub unsafe fn w_list_float_items_raw(obj: PyObjectRef) -> Option<(*mut f64, usiz Some((items.as_mut_ptr(), items.len())) } +/// listobject.py:2234 IntOrFloatListStrategy.sort and +/// listobject.py:2449 IntOrFloatSort.lt. Unlike the homogeneous raw-array +/// accessors above, the encoded `i64` values must be ordered after decoding. +/// Reverse follows PyPy's reverse/stable-sort/reverse sequence so equal +/// int/float values retain reverse-sort stability. +pub unsafe fn w_list_sort_int_or_float(obj: PyObjectRef, reverse: bool) -> bool { + let list = &mut *(obj as *mut W_ListObject); + if list.strategy != ListStrategy::IntOrFloat { + return false; + } + let items = list.int_items.as_mut_slice(); + if reverse { + items.reverse(); + } + items.sort_by(|a, b| { + int_or_float_as_float(*a) + .partial_cmp(&int_or_float_as_float(*b)) + .unwrap_or(std::cmp::Ordering::Equal) + }); + if reverse { + items.reverse(); + } + true +} + /// Whether the list still holds the EmptyListStrategy. /// /// `descr_sort` (listobject.py:873) uses this to tell whether the user mucked @@ -1958,6 +2223,7 @@ pub unsafe fn w_list_reverse(obj: PyObjectRef) { // (listobject.py defaults) which is a no-op for length 0. ListStrategy::Empty => {} ListStrategy::Integer => list.int_items.as_mut_slice().reverse(), + ListStrategy::IntOrFloat => list.int_items.as_mut_slice().reverse(), ListStrategy::Float => list.float_items.as_mut_slice().reverse(), ListStrategy::Object => list.object_reverse(), } @@ -1981,6 +2247,15 @@ pub unsafe fn w_list_delslice(obj: PyObjectRef, start: usize, end: usize) { changed = true; } } + ListStrategy::IntOrFloat => { + let len = list.int_items.len(); + let s = start.min(len); + let e = end.min(len); + if s < e { + list.int_items.drain(s..e); + changed = true; + } + } ListStrategy::Float => { let len = list.float_items.len(); let s = start.min(len); @@ -2131,6 +2406,35 @@ pub unsafe fn w_list_find_or_count_fast( ListFindFast::NotFound } } + // listobject.py:2280 IntOrFloatListStrategy._safe_find_or_count: + // compare raw longlongs first (same NaN payload), then decoded + // numeric values (0 == -0.0 and 42 == 42.0). + ListStrategy::IntOrFloat => { + let Some(target) = int_or_float_encode_item(w_item) else { + return ListFindFast::NeedsGeneric; + }; + let target_float = int_or_float_as_float(target); + let items = list.int_items.as_slice(); + let stop = stop.min(items.len() as i64); + let mut result = 0i64; + let mut i = start.max(0); + while i < stop { + let value = items[i as usize]; + if value == target || int_or_float_as_float(value) == target_float { + if count { + result += 1; + } else { + return ListFindFast::Found(i); + } + } + i += 1; + } + if count { + ListFindFast::Count(result) + } else { + ListFindFast::NotFound + } + } _ => ListFindFast::NeedsGeneric, } } @@ -2187,6 +2491,12 @@ unsafe fn w_list_setslice_inner( list.strategy = ListStrategy::Integer; return Ok(()); } + ListStrategy::IntOrFloat => { + let fresh = IntArray::from_vec(other.int_items.to_vec()); + list.int_items.install(fresh); + list.strategy = ListStrategy::IntOrFloat; + return Ok(()); + } ListStrategy::Float => { let fresh = FloatArray::from_vec(other.float_items.to_vec()); list.float_items.install(fresh); @@ -2201,6 +2511,55 @@ unsafe fn w_list_setslice_inner( } } } + // listobject.py:1998/2013 IntegerListStrategy and :2096/2110 + // FloatListStrategy first generalize themselves when the donor is a + // compatible numeric strategy, then re-dispatch the same setslice. + if list.strategy == ListStrategy::Integer + && matches!( + other.strategy, + ListStrategy::Float | ListStrategy::IntOrFloat + ) + && integer_to_int_or_float(list) + { + return w_list_setslice_inner(obj, start, end, w_other); + } + if list.strategy == ListStrategy::Float + && matches!( + other.strategy, + ListStrategy::Integer | ListStrategy::IntOrFloat + ) + && float_to_int_or_float(list) + { + return w_list_setslice_inner(obj, start, end, w_other); + } + // listobject.py:2254 IntOrFloatListStrategy.setslice converts an + // Integer/Float donor to temporary signed-longlong storage without + // de-specialising the receiver. + if list.strategy == ListStrategy::IntOrFloat + && matches!(other.strategy, ListStrategy::Integer | ListStrategy::Float) + { + let converted: Option> = match other.strategy { + ListStrategy::Integer => other + .int_items + .as_slice() + .iter() + .map(|&value| int_or_float_encode_int(value)) + .collect(), + ListStrategy::Float => other + .float_items + .as_slice() + .iter() + .map(|&value| int_or_float_encode_float(value)) + .collect(), + _ => unreachable!(), + }; + if let Some(converted) = converted { + let s = start.min(list.int_items.len()); + let e = end.min(list.int_items.len()); + list.int_items.splice(s, e - s, &converted); + return Ok(()); + } + } // listobject.py:1752: not self.list_is_correct_type(w_other) and w_other.length() != 0 // Only switch strategy when donor is non-empty AND has different type. // Empty donor → pure deletion, strategy preserved. @@ -2228,6 +2587,23 @@ unsafe fn w_list_setslice_inner( } return Ok(()); } + ListStrategy::IntOrFloat => { + let new_items = if list.strategy == other.strategy { + other.int_items.as_slice() + } else { + &[] + }; + let s = start.min(list.int_items.len()); + let e = end.min(list.int_items.len()); + if obj == w_other { + let mut v = list.int_items.to_vec(); + v.splice(s..e, new_items.iter().copied()); + list.int_items.install(IntArray::from_vec(v)); + } else { + list.int_items.splice(s, e - s, new_items); + } + return Ok(()); + } ListStrategy::Float => { let new_items = if list.strategy == other.strategy { other.float_items.as_slice() @@ -2499,32 +2875,93 @@ mod tests { } #[test] - fn test_list_setitem_mixed_value_switches_to_object_strategy() { + fn test_list_setitem_mixed_value_switches_to_int_or_float_strategy() { let list = w_list_new(vec![w_int_new(1), w_int_new(2)]); let float = crate::floatobject::w_float_new(3.5); unsafe { assert!(w_list_uses_int_storage(list)); assert!(w_list_setitem(list, 0, float)); - assert!(w_list_uses_object_storage(list)); + assert!(w_list_uses_int_or_float_storage(list)); let value = w_list_getitem(list, 0).unwrap(); assert!(crate::pyobject::is_float(value)); } } #[test] - fn test_list_append_mixed_value_switches_to_object_strategy() { + fn test_list_append_mixed_value_switches_to_int_or_float_strategy() { let list = w_list_new(vec![w_int_new(1), w_int_new(2)]); let float = crate::floatobject::w_float_new(3.5); unsafe { assert!(w_list_uses_int_storage(list)); w_list_append(list, float); - assert!(w_list_uses_object_storage(list)); + assert!(w_list_uses_int_or_float_storage(list)); assert_eq!(w_list_len(list), 3); let value = w_list_getitem(list, 2).unwrap(); assert!(crate::pyobject::is_float(value)); } } + #[test] + fn test_int_or_float_strategy_preserves_types_and_numeric_equality() { + let list = w_list_new(vec![ + w_int_new(42), + crate::floatobject::w_float_new(42.0), + crate::floatobject::w_float_new(-0.0), + ]); + unsafe { + assert!(w_list_uses_int_or_float_storage(list)); + let integer = w_list_getitem(list, 0).unwrap(); + let float = w_list_getitem(list, 1).unwrap(); + let negative_zero = w_list_getitem(list, 2).unwrap(); + assert!(crate::pyobject::is_int(integer)); + assert!(crate::pyobject::is_float(float)); + assert_eq!(crate::intobject::w_int_get_value(integer), 42); + assert_eq!(crate::floatobject::w_float_get_value(float), 42.0); + assert!(crate::floatobject::w_float_get_value(negative_zero).is_sign_negative()); + + assert!(matches!( + w_list_find_or_count_fast(list, w_int_new(42), 0, 3, true), + ListFindFast::Count(2) + )); + assert!(matches!( + w_list_find_or_count_fast(list, crate::floatobject::w_float_new(0.0), 0, 3, false), + ListFindFast::Found(2) + )); + } + } + + #[test] + fn test_int_or_float_rejects_out_of_int32_range() { + let list = w_list_new(vec![w_int_new(i32::MAX as i64 + 1), w_int_new(1)]); + unsafe { + assert!(w_list_uses_int_storage(list)); + w_list_append(list, crate::floatobject::w_float_new(2.5)); + assert!(w_list_uses_object_storage(list)); + } + } + + #[test] + fn test_int_or_float_setslice_accepts_integer_and_float_strategies() { + let list = w_list_new(vec![w_int_new(1), crate::floatobject::w_float_new(4.0)]); + let integers = w_list_new(vec![w_int_new(2), w_int_new(3)]); + let floats = w_list_new(vec![crate::floatobject::w_float_new(2.5)]); + unsafe { + w_list_setslice(list, 1, 1, integers).unwrap(); + assert!(w_list_uses_int_or_float_storage(list)); + w_list_setslice(list, 1, 3, floats).unwrap(); + assert!(w_list_uses_int_or_float_storage(list)); + assert_eq!(w_list_len(list), 3); + assert_eq!( + crate::floatobject::w_float_get_value(w_list_getitem(list, 1).unwrap()), + 2.5 + ); + assert_eq!( + crate::floatobject::w_float_get_value(w_list_getitem(list, 2).unwrap()), + 4.0 + ); + } + } + #[test] fn test_list_uses_float_strategy_for_homogeneous_floats() { let list = w_list_new(vec![ @@ -2543,7 +2980,7 @@ mod tests { } #[test] - fn test_list_setitem_mixed_on_float_strategy_switches_to_object_strategy() { + fn test_list_setitem_mixed_on_float_strategy_switches_to_int_or_float_strategy() { let list = w_list_new(vec![ crate::floatobject::w_float_new(1.0), crate::floatobject::w_float_new(2.0), @@ -2551,14 +2988,14 @@ mod tests { unsafe { assert!(w_list_uses_float_storage(list)); assert!(w_list_setitem(list, 0, w_int_new(7))); - assert!(w_list_uses_object_storage(list)); + assert!(w_list_uses_int_or_float_storage(list)); let value = w_list_getitem(list, 0).unwrap(); assert!(crate::pyobject::is_int(value)); } } #[test] - fn test_list_append_mixed_on_float_strategy_switches_to_object_strategy() { + fn test_list_append_mixed_on_float_strategy_switches_to_int_or_float_strategy() { let list = w_list_new(vec![ crate::floatobject::w_float_new(1.0), crate::floatobject::w_float_new(2.0), @@ -2566,7 +3003,7 @@ mod tests { unsafe { assert!(w_list_uses_float_storage(list)); w_list_append(list, w_int_new(7)); - assert!(w_list_uses_object_storage(list)); + assert!(w_list_uses_int_or_float_storage(list)); assert_eq!(w_list_len(list), 3); let value = w_list_getitem(list, 2).unwrap(); assert!(crate::pyobject::is_int(value)); @@ -2637,14 +3074,14 @@ mod tests { } #[test] - fn test_int_list_insert_float_switches_to_object() { + fn test_int_list_insert_float_switches_to_int_or_float() { // AbstractUnwrappedStrategy.switch_to_next_strategy (listobject.py:1720) let list = w_list_new(vec![w_int_new(1), w_int_new(2)]); let fv = crate::floatobject::w_float_new(9.0); unsafe { assert!(w_list_uses_int_storage(list)); w_list_insert(list, 1, fv); - assert!(w_list_uses_object_storage(list)); + assert!(w_list_uses_int_or_float_storage(list)); assert_eq!(w_list_len(list), 3); } } From cf40e9e46c0a0d4aaa508a15a54d575b7ca89c75 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 10 Aug 2026 05:30:32 +0900 Subject: [PATCH 24/26] jit: repair entry bridge and carrier regressions --- majit/majit-metainterp/src/pyjitpl.rs | 76 +++++++++++++++++-- .../synth/closure_per_call.wasm.jitstats | 3 +- ..._freevar_after_mayforce.cranelift.jitstats | 3 +- ...ine_freevar_after_mayforce.dynasm.jitstats | 3 +- ...nline_freevar_after_mayforce.wasm.jitstats | 3 +- .../bench/synth/list_pop_append.wasm.jitstats | 3 +- .../pypy_type_surface.cranelift.jitstats | 6 +- .../synth/pypy_type_surface.dynasm.jitstats | 6 +- .../synth/pypy_type_surface.wasm.jitstats | 6 +- .../pickletools_jit_carrier_abort.py | 23 ++++++ .../src/jitcode_dispatch/arith.rs | 30 +++++++- .../src/jitcode_dispatch/inline_call.rs | 29 +++++++ .../src/jitcode_dispatch/mod.rs | 3 +- .../src/jitcode_dispatch/tests.rs | 25 +++++- pyre/pyre-jit-trace/src/trace.rs | 12 ++- 15 files changed, 204 insertions(+), 27 deletions(-) create mode 100644 pyre/extra_tests/parity_tests/pickletools_jit_carrier_abort.py diff --git a/majit/majit-metainterp/src/pyjitpl.rs b/majit/majit-metainterp/src/pyjitpl.rs index 20506615165..95012d59fb5 100644 --- a/majit/majit-metainterp/src/pyjitpl.rs +++ b/majit/majit-metainterp/src/pyjitpl.rs @@ -7251,6 +7251,37 @@ impl MetaInterp { .enumerate() .map(|(i, &tp)| majit_ir::InputArg::from_type(tp, i as u32)) .collect(); + // compile.py:1006-1017 `ResumeFromInterpDescr.compile_and_attach` + // passes `orig_inputargs` through to `send_loop_to_backend`, which + // reads the concrete virtualizable before patching the expanded loop + // entry. Capture both pieces while the originating TraceCtx is still + // live; after this method returns to `compile_entry_bridge`, an + // ambient `active_jitdriver_sd` / `vable_ptr` can belong to an inlined + // callee and is not an admissible substitute for this frame. + let entry_driver_descriptor = ctx.driver_descriptor().cloned(); + let entry_orig_vable_ptr = if entry_bridge.is_some() { + let from_initial_args = entry_driver_descriptor + .as_ref() + .and_then(|driver| driver.virtualizable_arg_index()) + .and_then(|idx| ctx.initial_inputarg_consts.get(idx)) + .and_then(|value| match value { + OpRef::ConstPtr(reference) if !reference.is_null() => { + Some(reference.0 as *const u8) + } + _ => None, + }); + from_initial_args + .or_else(|| match ctx.standard_virtualizable_concrete() { + Some(Value::Ref(reference)) if !reference.is_null() => { + Some(reference.as_usize() as *const u8) + } + _ => None, + }) + .or_else(|| ctx.virtualizable_heap_ptr()) + .unwrap_or(std::ptr::null()) + } else { + std::ptr::null() + }; // The recorder carries Const values inline on the OpRef variants // (history.py:227/268/314), so there is no legacy TraceCtx // ConstantPool to snapshot — this typed-constant map starts fresh. @@ -7378,6 +7409,8 @@ impl MetaInterp { green_key, original_green_key, entry_meta, + entry_driver_descriptor, + entry_orig_vable_ptr, &bridge_ops, &bridge_inputargs, bridge_constants, @@ -11386,6 +11419,8 @@ impl MetaInterp { green_key: u64, original_green_key: u64, meta: M, + driver_descriptor: Option, + orig_vable_ptr_entry: *const u8, bridge_ops: &[majit_ir::Op], bridge_inputargs: &[majit_ir::InputArg], bridge_constants: majit_ir::ConstMap, @@ -11593,6 +11628,23 @@ impl MetaInterp { } let mut optimized_ops = compile::strip_stray_overflow_guards(optimized_ops); + let mut entry_inputargs: Vec = bridge_inputargs + .iter() + .map(InputArg::fresh_value_copy) + .collect(); + // compile.py:1014-1017 -> send_loop_to_backend(..., orig_inputargs): + // entry bridges are loops installed as an interpreter front door, so + // they require the same virtualizable-field reload preamble and + // reds-only input contract as ordinary root loops. Compiling the + // optimizer's expanded input list directly makes execute_token pass + // two red values to a loop expecting dozens of frame-field slots. + self.patch_new_loop_to_load_virtualizable_fields( + &mut entry_inputargs, + &mut optimized_ops, + &mut constants, + driver_descriptor.as_ref(), + orig_vable_ptr_entry, + ); let num_optimized_ops = optimized_ops.len(); let compiled_constants_typed = crate::optimizeopt::optimizer::lower_typed_constants_to_const_pool(&constants); @@ -11601,7 +11653,7 @@ impl MetaInterp { if crate::majit_log_enabled() { eprintln!( "[jit][entry-bridge] original_key={} target_key={} inputs={:?}", - original_green_key, green_key, bridge_inputargs + original_green_key, green_key, entry_inputargs ); for (i, op) in optimized_ops.iter().enumerate() { eprintln!( @@ -11628,10 +11680,18 @@ impl MetaInterp { self.backend .set_next_frame_value_count_fn(self.active_frame_value_count_fn()); - let token = make_jitcell_token(self.warm_state.alloc_token_number(), None); + let token = make_jitcell_token( + self.warm_state.alloc_token_number(), + driver_descriptor.as_ref().and_then(|driver| driver.index), + ); // `green_key` is interior-mutable, so it is written through the // shared `Arc`. token.green_key.set(original_green_key); + self.configure_loop_token_for_driver( + token.as_ref(), + original_green_key, + driver_descriptor.as_ref(), + ); // compile.py:532-546 `debug_start("jit-backend") + // profiler.start_backend() ... try: do_compile_loop ... finally: @@ -11641,7 +11701,7 @@ impl MetaInterp { let _backend_scope = self.staticdata.profiler.enter_backend(); std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { self.backend - .compile_loop(bridge_inputargs, &optimized_ops, &token) + .compile_loop(&entry_inputargs, &optimized_ops, &token) })) }; let compile_time = Instant::now().saturating_duration_since(compile_start); @@ -11661,13 +11721,13 @@ impl MetaInterp { // compile.py:213 record_loop_or_bridge. self.record_loop_or_bridge(&token, &optimized_ops, trace_id); let (mut resume_data, mut exit_layouts) = compile::build_guard_metadata( - bridge_inputargs, + &entry_inputargs, &optimized_ops, original_green_key, self.active_frame_value_count_fn(), ); let mut terminal_exit_layouts = - compile::build_terminal_exit_layouts(bridge_inputargs, &optimized_ops); + compile::build_terminal_exit_layouts(&entry_inputargs, &optimized_ops); if let Some(backend_layouts) = self.backend.compiled_fail_descr_layouts(token.as_ref()) { @@ -11691,7 +11751,7 @@ impl MetaInterp { &mut resume_data, &mut exit_layouts, trace_id, - bridge_inputargs, + &entry_inputargs, trace_info.as_ref(), ); compile::patch_guard_recovery_layouts_for_trace(&mut exit_layouts); @@ -11702,12 +11762,12 @@ impl MetaInterp { &mut terminal_exit_layouts, ); let mut next_global_opref = - compute_next_global_opref(bridge_inputargs, &optimized_ops); + compute_next_global_opref(&entry_inputargs, &optimized_ops); let mut traces = indexmap::IndexMap::new(); traces.insert( trace_id, CompiledTrace { - inputargs: bridge_inputargs + inputargs: entry_inputargs .iter() .map(InputArg::fresh_value_copy) .collect(), diff --git a/pyre/bench/synth/closure_per_call.wasm.jitstats b/pyre/bench/synth/closure_per_call.wasm.jitstats index 9128a727b22..c30972e146e 100644 --- a/pyre/bench/synth/closure_per_call.wasm.jitstats +++ b/pyre/bench/synth/closure_per_call.wasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=418 +guard_failures=417 internal_compile_panics=0 loops_aborted=0 loops_compiled=4 +retraces_compiled=0 diff --git a/pyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstats b/pyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstats index e5429a42137..e593548d29b 100644 --- a/pyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstats +++ b/pyre/bench/synth/inline_freevar_after_mayforce.cranelift.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=930 +guard_failures=923 internal_compile_panics=0 loops_aborted=0 loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/inline_freevar_after_mayforce.dynasm.jitstats b/pyre/bench/synth/inline_freevar_after_mayforce.dynasm.jitstats index e5429a42137..e593548d29b 100644 --- a/pyre/bench/synth/inline_freevar_after_mayforce.dynasm.jitstats +++ b/pyre/bench/synth/inline_freevar_after_mayforce.dynasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=930 +guard_failures=923 internal_compile_panics=0 loops_aborted=0 loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstats b/pyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstats index e5429a42137..e593548d29b 100644 --- a/pyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstats +++ b/pyre/bench/synth/inline_freevar_after_mayforce.wasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=930 +guard_failures=923 internal_compile_panics=0 loops_aborted=0 loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/list_pop_append.wasm.jitstats b/pyre/bench/synth/list_pop_append.wasm.jitstats index 1d9aa27c053..651a3eaf3e9 100644 --- a/pyre/bench/synth/list_pop_append.wasm.jitstats +++ b/pyre/bench/synth/list_pop_append.wasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=3 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 loops_compiled=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/pypy_type_surface.cranelift.jitstats b/pyre/bench/synth/pypy_type_surface.cranelift.jitstats index 81c8187a9e3..ebea74eff3b 100644 --- a/pyre/bench/synth/pypy_type_surface.cranelift.jitstats +++ b/pyre/bench/synth/pypy_type_surface.cranelift.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=5 +bridges_compiled=3 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,8 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=1011 +guard_failures=604 internal_compile_panics=0 loops_aborted=0 -loops_compiled=11 +loops_compiled=7 retraces_compiled=0 diff --git a/pyre/bench/synth/pypy_type_surface.dynasm.jitstats b/pyre/bench/synth/pypy_type_surface.dynasm.jitstats index 81c8187a9e3..ebea74eff3b 100644 --- a/pyre/bench/synth/pypy_type_surface.dynasm.jitstats +++ b/pyre/bench/synth/pypy_type_surface.dynasm.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=5 +bridges_compiled=3 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,8 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=1011 +guard_failures=604 internal_compile_panics=0 loops_aborted=0 -loops_compiled=11 +loops_compiled=7 retraces_compiled=0 diff --git a/pyre/bench/synth/pypy_type_surface.wasm.jitstats b/pyre/bench/synth/pypy_type_surface.wasm.jitstats index 81c8187a9e3..ebea74eff3b 100644 --- a/pyre/bench/synth/pypy_type_surface.wasm.jitstats +++ b/pyre/bench/synth/pypy_type_surface.wasm.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=5 +bridges_compiled=3 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,8 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=1011 +guard_failures=604 internal_compile_panics=0 loops_aborted=0 -loops_compiled=11 +loops_compiled=7 retraces_compiled=0 diff --git a/pyre/extra_tests/parity_tests/pickletools_jit_carrier_abort.py b/pyre/extra_tests/parity_tests/pickletools_jit_carrier_abort.py new file mode 100644 index 00000000000..8b89d68f7ad --- /dev/null +++ b/pyre/extra_tests/parity_tests/pickletools_jit_carrier_abort.py @@ -0,0 +1,23 @@ +import pickle +import pickletools +import sys + + +# Warm the nested optimize/framer paths into compiled code, then exercise the +# multi-frame carrier-abort shape that used to discard DoneWithThisFrameRef and +# make optimize() fall through with None. +for proto in range(pickle.HIGHEST_PROTOCOL + 1): + for n in range(70): + optimized = pickletools.optimize(pickle.dumps(2**n, proto)) + assert optimized + len(optimized) + +for proto in range(pickle.HIGHEST_PROTOCOL + 1): + n = sys.maxsize + while n: + for expected in (-n, n): + optimized = pickletools.optimize(pickle.dumps(expected, proto)) + assert pickle.loads(optimized) == expected + n >>= 1 + +print("OK") diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/arith.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/arith.rs index 3670327bdda..0a35a92c9c3 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/arith.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/arith.rs @@ -761,8 +761,8 @@ pub(crate) fn record_float_cmp( } /// Bank-crossing unary cast family from the `pyjitpl.py` -/// exec-generated unary loop (`cast_int_to_float` / `cast_int_to_ptr` -/// / `cast_ptr_to_int`). PyPy generates these from one template +/// exec-generated unary loop (`cast_float_to_int` / `cast_int_to_float` / +/// `cast_int_to_ptr` / `cast_ptr_to_int`). PyPy generates these from one template /// because its boxes are untyped; pyre's typed register banks make /// each cast a distinct (src-bank, dst-bank, concrete-fold) triple, so /// the recorded `opcode` selects the shape. Operand layout `>` @@ -777,6 +777,31 @@ pub(crate) fn unop_cast_record( let dst = code[op.pc + 2] as usize; count_ops_executed(ctx, opcode); match opcode { + // `cast_float_to_int/f>i`: Float-bank → Int-bank. This is the + // first member of RPython's generated unary cast family + // (`pyjitpl.py:357`) and uses the same concrete operation as + // `blackhole.py bhimpl_cast_float_to_int`. + OpCode::CastFloatToInt => { + let a = read_float_reg(code, op, 0, ctx)?; + let result = if let Some(majit_ir::Value::Float(f)) = a.inline_const_to_value() + && let Some(majit_ir::Value::Int(n)) = + majit_metainterp::executor::execute_cast_const( + opcode, + majit_ir::Value::Float(f), + ) { + ctx.trace_ctx.const_int(n) + } else { + count_ops_recorded(ctx, opcode); + let result = ctx.trace_ctx.record_op(opcode, &[a]); + if let Some(majit_ir::Value::Float(f)) = ctx.trace_ctx.box_value(a) { + ctx.trace_ctx + .set_opref_concrete(result, majit_ir::Value::Int(f as i64)); + } + result + }; + let concrete_for_shadow = concrete_from_recorded_opref(ctx, result); + write_int_reg(ctx, op.pc, dst, result, concrete_for_shadow)?; + } // `cast_int_to_float/i>f`: Int-bank → Float-bank. Stamp the // result with the operand's Box.value as an f64 so downstream // `box_value(result)` callers see the live value. @@ -1077,6 +1102,7 @@ regular_record_table! { // shape from the opcode (pyre's typed banks cannot share one template // the way PyPy's untyped boxes do). unop_cast_record { + "cast_float_to_int/f>i" => CastFloatToInt, "cast_int_to_float/i>f" => CastIntToFloat, "cast_int_to_ptr/i>r" => CastIntToPtr, "cast_ptr_to_int/r>i" => CastPtrToInt, diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs index 85016e3f962..999cd4bca6d 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs @@ -6781,6 +6781,35 @@ pub(crate) fn dispatch_inline_call_dir_kind( let (ref_args, ref_width) = read_ref_var_list(code, op, 2 + int_width, ctx)?; let ref_arg_concretes = read_ref_var_list_concrete(code, op, 2 + int_width, ctx); + // RPython `rclass.py`/`rbuiltin.py` allocation lowering: entering the + // canonical `w_int_new` helper with one signed argument records the + // `NEW_WITH_VTABLE + SETFIELD_GC` box directly. The LLBC fallback body + // spells the same operation as a zero-argument allocation residual plus + // header/payload stores; its generic allocator has no host fnaddr (it is + // an lltype allocation opcode, not a callable), so descending that legacy + // spelling would abort at the symbolic funcptr. Treat the translated + // helper call as the allocation intrinsic at this frame boundary, exactly + // where RPython's rtyper has already replaced `malloc(W_IntObject)`. + let is_w_int_new = crate::jitcode_runtime::get_jitcode_ref_by_index(sub_index) + .is_some_and(|jc| jc.name == "w_int_new" && jc.code.as_ptr() == sub_body.code.as_ptr()); + if is_w_int_new + && dst_bank == 'r' + && int_args.len() == 1 + && ref_args.is_empty() + && let Some(ConcreteValue::Int(value)) = int_arg_concretes.first().copied() + { + let boxed_ptr = pyre_object::w_int_new(value) as i64; + let boxed = walker_box_int(ctx, op.pc, int_args[0], value)?; + let boxed_concrete = box_int_concrete(value, boxed_ptr); + ctx.trace_ctx.set_opref_concrete(boxed, boxed_concrete); + let dst = code[op.pc + 1 + 2 + int_width + ref_width] as usize; + let ConcreteValue::Ref(boxed_shadow) = concrete_from_recorded_opref(ctx, boxed) else { + unreachable!("box_int_concrete must produce a Ref concrete") + }; + write_ref_reg(ctx, op.pc, dst, boxed, ConcreteValue::Ref(boxed_shadow))?; + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + let callee_outcome = run_sub_jitcode_walk( ctx, op.pc, diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs index b1306d31ae9..360f0636a34 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs @@ -9927,7 +9927,8 @@ fn handle( // long_mod / long_div until the build-pipeline jtransform // port lands) get a `setdefault`-allocated dynamic byte and // resolve through BH dispatch only. - // `cast_int_to_float` / `cast_int_to_ptr` / `cast_ptr_to_int` + // `cast_float_to_int` / `cast_int_to_float` / `cast_int_to_ptr` / + // `cast_ptr_to_int` // route through `dispatch_regular_record` (see `arith.rs` // `unop_cast_record`) — part of the `pyjitpl.py` // exec-generated unary family. diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs index cc64bb0a14a..afbef1ee384 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/tests.rs @@ -2388,7 +2388,9 @@ fn pop_end_journal_rollback_after_strategy_switch() { #[test] fn interleaved_append_pop_journal_rollback_restores_original() { - use pyre_object::listobject::{W_ListObject, ll_list_int_getitem_fast, w_list_len, w_list_new}; + use pyre_object::listobject::{ + W_ListObject, ll_list_int_getitem_fast, w_list_allocated, w_list_len, w_list_new, + }; use pyre_object::{w_int_new, w_list_append, w_list_pop_end}; super::fbw_store_journal_reset(); @@ -6878,6 +6880,27 @@ fn cast_int_to_float_folds_a_const_int_without_recording() { ); } +#[test] +fn cast_float_to_int_folds_a_const_float_without_recording() { + let byte = *insns_opname_to_byte() + .get("cast_float_to_int/f>i") + .expect("`cast_float_to_int/f>i` must be in insns table"); + let code = [byte, 0x00, 0x00]; // `f>i`: f-src=0, i-dst=0 + let mut tc = fresh_trace_ctx(); + let operand = tc.const_float((42.75f64).to_bits() as i64); + let mut regs_f = [operand]; + let mut regs_i = [OpRef::None]; + let (_, next_pc) = run_float_step(&code, &mut tc, &mut regs_f, &mut regs_i) + .expect("cast_float_to_int on a const float must fold"); + assert_eq!(next_pc, 3); + assert_eq!(tc.num_ops(), 0, "a const operand folds without recording"); + assert_eq!( + regs_i[0].inline_const_to_value(), + Some(majit_ir::Value::Int(42)), + "dst must hold the folded ConstInt", + ); +} + /// `abort/>r` is a pyre-only no-op result marker — the walker /// counterpart of blackhole's `handler_abort_result_marker_r` /// (`blackhole.rs`). No operand read, no register write, no diff --git a/pyre/pyre-jit-trace/src/trace.rs b/pyre/pyre-jit-trace/src/trace.rs index 4c6c0ab5d4c..68fbfea02d2 100644 --- a/pyre/pyre-jit-trace/src/trace.rs +++ b/pyre/pyre-jit-trace/src/trace.rs @@ -2058,12 +2058,22 @@ fn drive_bridge_carrier_walk( } discard_bridge_carrier_walk(ctx, sym, entry_depth, pre_pos, &pre_virtualref_boxes); crate::jitcode_dispatch::bool_box_truth_reset(); - crate::jitcode_dispatch::fbw_finish_payload_reset(); if adopted { + // `try_adopt_blackhole` mirrors `convert_and_run_from_pyjitpl` and can + // finish the root frame with `DoneWithThisFrame*`; it records that + // terminal in `FBW_FINISH_CONCRETE` for the bridge launcher to raise + // back to the caller. Do not erase it while discarding the carrier's + // tracing-only state. Doing so adopted a frame positioned after + // RETURN_VALUE but lost its result, so the interpreter fell through + // with `None` (`pickletools.optimize` after the carrier abort). // The chain ran the callee forward from where the sub-walk stopped, so // the eager stores it journaled stand exactly once. crate::jitcode_dispatch::fbw_store_journal_commit(); } else { + // No blackhole terminal belongs to this walk. Clear any sub-walk + // payload before the guard-state replay, as the old unconditional + // reset did. + crate::jitcode_dispatch::fbw_finish_payload_reset(); // Non-commit epilogue: the sub-walk concrete-executed the reconstructed // callee, and the blackhole replays it from the guard, so restore the // pre-walk heap rather than dropping the journals (which would leave every From bfa00bc8a8d5717198f13c5ecf526e5e121109a0 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 10 Aug 2026 07:00:24 +0900 Subject: [PATCH 25/26] jit: fix blackhole catches and void call results --- majit/majit-metainterp/src/blackhole.rs | 37 +++++-------------- .../src/codewriter/jtransform.rs | 29 +++++++++------ .../pickle_fresh_pickler_memo_jit.py | 18 +++++++++ 3 files changed, 45 insertions(+), 39 deletions(-) create mode 100644 pyre/extra_tests/parity_tests/pickle_fresh_pickler_memo_jit.py diff --git a/majit/majit-metainterp/src/blackhole.rs b/majit/majit-metainterp/src/blackhole.rs index ef86084e0dc..7243351b61f 100644 --- a/majit/majit-metainterp/src/blackhole.rs +++ b/majit/majit-metainterp/src/blackhole.rs @@ -1213,14 +1213,12 @@ impl BlackholeInterpreter { if opcode == self.op_catch_exception { return self.route_to_catch(position, exc_value); } - // A guard resume coordinate may name the successor block's entry - // `-live-`, before that block mirrors the virtualizable and reaches - // the raising operation's trailing `-live-`. The flattener emits the - // `catch_exception` immediately after that trailing marker - // (`flatten.py:206-217`). Walk forward to the first such marker and - // accept only its immediately-following catch; crossing any other - // operation after it means this exception belongs to no handler at - // the resumed call site. + // A guard resume coordinate can name the successor block entry before + // the translated raising operation. PyPy's MIFrame position already + // names the operation's trailing live/catch pair; pyre recovers that + // generated-coordinate gap by scanning to the FIRST such pair. Ops + // before the trailing live belong to that translated operation. Once + // the live is crossed, only its immediately-following catch is valid. if let Some(catch_pos) = self.find_catch_after_resume_live(resume_live_pos) { return self.route_to_catch(catch_pos, exc_value); } @@ -1396,23 +1394,6 @@ impl BlackholeInterpreter { } if op == self.op_live { crossed_trailing_live = true; - continue; - } - if !matches!( - op, - majit_translate::insns::BC_SETFIELD_VABLE_I - | majit_translate::insns::BC_SETFIELD_VABLE_R - | majit_translate::insns::BC_SETFIELD_VABLE_F - | majit_translate::insns::BC_SETARRAYITEM_VABLE_I - | majit_translate::insns::BC_SETARRAYITEM_VABLE_R - | majit_translate::insns::BC_SETARRAYITEM_VABLE_F - ) { - // Only the codewriter's successor-block virtualizable mirror - // stores may separate the guard resume coordinate from the - // raising operation's trailing live marker. Crossing an - // arbitrary operation would attach its exception to the next - // operation's handler and silently swallow it. - return None; } } None @@ -4368,7 +4349,7 @@ mod tests { } #[test] - fn test_guard_exception_resume_does_not_cross_another_operation() { + fn test_guard_exception_resume_crosses_translated_operation() { let mut asm = majit_translate::codewriter::assembler::Assembler::new(); let mut b = JitCodeBuilder::default(); let resume_pc = b.current_pos(); @@ -4378,6 +4359,7 @@ mod tests { let handler_lbl = b.new_label(); b.catch_exception(handler_lbl); b.mark_label(handler_lbl); + let handler_pc = b.current_pos(); b.int_return(0); let jitcode = b.finish(); @@ -4385,7 +4367,8 @@ mod tests { let mut bh = builder.acquire_interp(); bh.setposition(std::sync::Arc::new(jitcode), resume_pc); - assert!(!bh.handle_exception_in_frame(0xCAFE_F00D)); + assert!(bh.handle_exception_in_frame(0xCAFE_F00D)); + assert_eq!(bh.position, handler_pc); } thread_local! { diff --git a/majit/majit-translate/src/codewriter/jtransform.rs b/majit/majit-translate/src/codewriter/jtransform.rs index 6033a2974df..621c4a07bb3 100644 --- a/majit/majit-translate/src/codewriter/jtransform.rs +++ b/majit/majit-translate/src/codewriter/jtransform.rs @@ -3582,18 +3582,23 @@ impl<'a> Transformer<'a> { let kind = cc.guess_call_kind(op); return match kind { crate::call::CallKind::Regular => { - // No `effective_call_result_ty` reconciliation here (only - // the Residual arm below): an INLINED callee threads its - // `Result<(), PyError>` unit `()` result as a live - // `Ref`-carried value — a block inputarg / link arg the - // regalloc must colour — not a droppable void. Retyping - // it to `Void` leaves an uncolourable carried variable - // that `assembler.rs` `lookup_coloring` rejects. The - // residual case is different: `residual_call_v` genuinely - // drops the result slot, so the reconciliation is safe - // (and required) only there. Keep the front-derived - // `result_ty`. - self.handle_regular_call(op, target, args, result_ty, graph_name, graph) + // RPython call.py:230: the call result must have the same + // concretetype as FUNC.RESULT. In particular, after + // exceptiontransform a `Result<(), PyError>` callee has a + // void RESULT; emitting `inline_call_r_r` for the front's + // aggregate-shaped unit shell would disagree with the + // callee's `void_return`. Reconcile it just like the + // residual-call arm below. + let effective_result_ty = + self.effective_call_result_ty(target, op.result.as_ref(), result_ty); + self.handle_regular_call( + op, + target, + args, + &effective_result_ty, + graph_name, + graph, + ) } crate::call::CallKind::Residual => { // RPython jtransform.py:456-471: diff --git a/pyre/extra_tests/parity_tests/pickle_fresh_pickler_memo_jit.py b/pyre/extra_tests/parity_tests/pickle_fresh_pickler_memo_jit.py new file mode 100644 index 00000000000..b41cf60eebf --- /dev/null +++ b/pyre/extra_tests/parity_tests/pickle_fresh_pickler_memo_jit.py @@ -0,0 +1,18 @@ +import io +import pickle + + +# A fresh pure-Python Pickler must write the object before it can emit a memo +# reference. The JIT used to trace BytesIO.write(), abort while descending +# into its void-returning check_closed(), and replay Pickler.dump() after the +# first execution had already populated memo. The replay then emitted only a +# BINGET for a memo entry absent from the output stream. +for proto in range(pickle.HIGHEST_PROTOCOL + 1): + for n in range(100): + payload = bytes((n & 0xFF, proto)) + stream = io.BytesIO() + pickler = pickle._Pickler(stream, protocol=proto) + pickler.dump(payload) + assert pickle.loads(stream.getvalue()) == payload + +print("OK") From 8650fc5c026d7a4097656b8ac99af1d0b7c57bd2 Mon Sep 17 00:00:00 2001 From: "Jeong, YunWon" Date: Mon, 10 Aug 2026 08:20:20 +0900 Subject: [PATCH 26/26] tests: scope POSIX-only parity fixtures --- .../parity_tests/open_non_inheritable.py | 12 ++++++- .../parity_tests/socket_timeout_identity.py | 14 ++++++-- .../parity_tests/type_new_metatype_guard.py | 33 ++++++++++--------- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/pyre/extra_tests/parity_tests/open_non_inheritable.py b/pyre/extra_tests/parity_tests/open_non_inheritable.py index db0fcb2169c..faa5728bd5e 100644 --- a/pyre/extra_tests/parity_tests/open_non_inheritable.py +++ b/pyre/extra_tests/parity_tests/open_non_inheritable.py @@ -1,5 +1,15 @@ import os -import tempfile + + +# The implementation exercised here is the POSIX `fcntl(FD_CLOEXEC)` port. +# Windows pyre currently uses the non-fd pathname-backed FileIO carrier, so it +# has no descriptor whose inheritance flag this fixture could inspect. +if os.name != "posix": + print("OK") + raise SystemExit + + +import tempfile # noqa: E402 fd, path = tempfile.mkstemp() diff --git a/pyre/extra_tests/parity_tests/socket_timeout_identity.py b/pyre/extra_tests/parity_tests/socket_timeout_identity.py index 30a122169f8..e2c6dcb2a22 100644 --- a/pyre/extra_tests/parity_tests/socket_timeout_identity.py +++ b/pyre/extra_tests/parity_tests/socket_timeout_identity.py @@ -1,5 +1,15 @@ -import _socket -import socket +import os + + +# pyre's socket type and the stdlib wrapper it enables are currently a POSIX +# module (`interp_socket.rs` registers `socket` under `#[cfg(unix)]`). +if os.name != "posix": + print("OK") + raise SystemExit + + +import _socket # noqa: E402 +import socket # noqa: E402 assert _socket.timeout is TimeoutError diff --git a/pyre/extra_tests/parity_tests/type_new_metatype_guard.py b/pyre/extra_tests/parity_tests/type_new_metatype_guard.py index 7ec44f0ce98..ec854a0be2c 100644 --- a/pyre/extra_tests/parity_tests/type_new_metatype_guard.py +++ b/pyre/extra_tests/parity_tests/type_new_metatype_guard.py @@ -31,6 +31,8 @@ reason. """ +import os + def expect_type_error(label, fn, shared=""): """Assert `fn` is refused with a TypeError whose message contains `shared`.""" @@ -169,21 +171,20 @@ class ViaSuper(metaclass=SuperMeta): # The allocation check reads the metatype's recorded instance layout, so a # metaclass an extension module defines has to record that its instances are -# type objects. Importing `ctypes` alone exercises it — `class -# py_object(_SimpleCData)` is a `type.__new__` under one of those metaclasses — -# and the rows below name the metatype rather than assert its spelling, which -# the runtimes disagree on. -import ctypes # noqa: E402 - -CTYPES_META = type(ctypes.py_object) - -expect_value("ctypes_metatype_is_a_type", lambda: isinstance(CTYPES_META, type), True) -expect_value("ctypes_metatype_subtypes_type", lambda: issubclass(CTYPES_META, type), True) -expect_value("ctypes_subclass_name", lambda: type("P", (ctypes.py_object,), {}).__name__, "P") -expect_value( - "ctypes_subclass_metatype", - lambda: type(type("P", (ctypes.py_object,), {})), - CTYPES_META, -) +# type objects. Pyre's real ctypes types are currently the POSIX + host_env +# implementation; non-POSIX builds deliberately expose only an import stub. +if os.name == "posix": + import ctypes # noqa: E402 + + CTYPES_META = type(ctypes.py_object) + + expect_value("ctypes_metatype_is_a_type", lambda: isinstance(CTYPES_META, type), True) + expect_value("ctypes_metatype_subtypes_type", lambda: issubclass(CTYPES_META, type), True) + expect_value("ctypes_subclass_name", lambda: type("P", (ctypes.py_object,), {}).__name__, "P") + expect_value( + "ctypes_subclass_metatype", + lambda: type(type("P", (ctypes.py_object,), {})), + CTYPES_META, + ) print("OK")