Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion majit/majit-backend-cranelift/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17301,7 +17301,7 @@ mod tests {
/// GC_STORE against callee jitframes. Mirrors the dynasm test layout
/// (runner.rs install_call_assembler_test_layout); the offsets match
/// the production cranelift mapping in pyre-jit call_jit.rs
/// arena_jitframe_descrs. Reads jitframe_gc_type_id() after
/// jitframe_layout_descrs. Reads jitframe_gc_type_id() after
/// set_gc_allocator has lazily registered JITFRAME.
fn install_call_assembler_test_layout() {
register_jitframe_layout(JitFrameLayoutInfo {
Expand Down
8 changes: 1 addition & 7 deletions pyre/bench/synth/comprehension_object_append_hot.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
# No `max-pypy-ratio` gate: pypy runs this in ~0.019s, two ticks of the 10ms
# user-CPU resolution, so the ratio's own granularity is ~30-50%. A run where
# pyre got FASTER (1.22s -> 1.09s) still reddened a ratio=40 gate, because pypy
# happened to measure 0.03s instead of 0.04s. Raising the bound far enough to
# absorb one tick leaves it too loose to catch anything, so the bench keeps only
# its output check — which is what it was written for. The comprehension's flat
# constant factor against pypy is tracked separately.
# pyre-check: max-pypy-ratio=32
# An inlined list comprehension whose LIST_APPEND element lands in a list
# Object-strategy (tuple / None / str / dict / f-string) folds through the #171
# orthodox append. Its Object arm stores a GC ref and runs list_write_barrier,
Expand Down
18 changes: 18 additions & 0 deletions pyre/pyre-interpreter/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,24 @@ pub(crate) unsafe fn builtin_subclass_dunder_obj(
if w_class.is_null() || !pyre_object::is_type(w_class) {
return Ok(None);
}
// Only a subclass can redirect the dunder: it keeps the builtin
// `ob_type` and retags `w_class` (`typedef::subclass_to_tag`), which
// is exactly what `is_exact_builtin_instance` tests. An exact
// instance resolves the dunder to the builtin the caller is about to
// run natively, and the builtin types are immutable, so the two MRO
// walks and the descriptor call below can only reproduce it.
//
// `long` is the one leaf where the descriptor does more than the leaf
// formatter: `longobject.py descr_repr` also enforces
// `sys.set_int_max_str_digits`, and that check sits in the descriptor
// rather than in the conversion, so an exact `long` keeps going
// through it. A machine `int` cannot reach any settable limit — 19
// digits against a floor of 640.
if !std::ptr::eq(tp, &LONG_TYPE as *const PyType)
&& pyre_object::is_exact_builtin_instance(obj)
{
return Ok(None);
}
let Some((src, found)) = crate::baseobjspace::lookup_where_pair(w_class, name) else {
return Ok(None);
};
Expand Down
2 changes: 2 additions & 0 deletions pyre/pyre-interpreter/src/executioncontext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,8 @@ impl ExecutionContext {
// builtins_module, matching a fresh PyPy ExecutionContext.
ec.builtin_dict_cache = std::cell::Cell::new(pyre_object::PY_NULL);
ec.sys_exc_value = pyre_object::PY_NULL;
// executioncontext.py:53 — a fresh ExecutionContext starts at 0.
ec.coroutine_origin_tracking_depth = 0;
ec.current_gen_or_coroutine = pyre_object::PY_NULL;
ec.w_asyncgen_firstiter_fn = pyre_object::PY_NULL;
ec.w_asyncgen_finalizer_fn = pyre_object::PY_NULL;
Expand Down
4 changes: 2 additions & 2 deletions pyre/pyre-interpreter/src/importing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,8 @@ pub fn mount_embedded_stdlib(mount: &Path) {
// PyPy equivalent: `space.sys.get('modules')`, `space.sys.path`, and
// `space.builtin_modules` are object-space/process state, shared by every
// ExecutionContext. Raw GC references use the established process-global
// `usize` representation; the GIL serializes semantic access while the mutex
// also makes foreign STW root walks well-defined.
// `usize` representation; the mutex serializes semantic access and keeps
// foreign STW root walks well-defined.
static SYS_MODULES: LazyLock<Mutex<HashMap<String, usize>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static SYS_MODULES_DICT: AtomicUsize = AtomicUsize::new(0);
Expand Down
43 changes: 29 additions & 14 deletions pyre/pyre-interpreter/src/module/thread/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,11 +707,20 @@ mod handle_class {
if is_none(timeout) {
None
} else if is_float(timeout) {
Some(Duration::from_secs_f64(
floatobject::w_float_get_value(timeout).max(0.0),
))
// os_lock.py:33-39 parse_acquire_args — a timeout past the
// microsecond clock's range is an OverflowError, never a
// native abort. The negated comparison rejects NaN too.
let secs = floatobject::w_float_get_value(timeout);
if !(secs <= TIMEOUT_MAX) {
return Err(crate::PyError::overflow_error("timeout value is too large"));
}
Some(Duration::from_secs_f64(secs.max(0.0)))
} else if is_int(timeout) {
Some(Duration::from_secs(w_int_get_value(timeout).max(0) as u64))
let secs = w_int_get_value(timeout);
if secs as f64 > TIMEOUT_MAX {
return Err(crate::PyError::overflow_error("timeout value is too large"));
}
Some(Duration::from_secs(secs.max(0) as u64))
} else {
return Err(crate::PyError::type_error(
"timeout must be a number or None",
Expand Down Expand Up @@ -857,6 +866,18 @@ mod local_class {
#[staticmethod]
fn __new__(cls: PyObjectRef, args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
crate::typedef::check_user_subclass(type_object(), cls)?;
// os_local.py:81 rejects construction arguments before
// `allocate_instance`, and does so for every subtype that inherits
// `object.__init__` — not just for the exact type — so a refused
// construction never reaches `_register_in_ec` (os_local.py:40).
if args.len() > 1
&& unsafe { crate::baseobjspace::lookup_where_class_uncached(cls, "__init__") }
== Some(crate::typedef::w_object())
{
return Err(crate::PyError::type_error(
"Initialization arguments are not supported",
));
}
// os_local.py installs the first dictionary before app-level
// __init__ is entered, preventing recursive initialization.
let dicts = pyre_object::w_dict_new();
Expand All @@ -872,15 +893,6 @@ mod local_class {
});
unsafe { (*obj).w_class = cls };
register_local_in_current_ec(obj);

// The base `_local` has no app-level initializer accepting arguments.
// Subclass initialization is dispatched by the ordinary type call
// after this allocator returns, exactly as for PyPy's TypeDef.
if args.len() > 1 && cls == type_object() {
return Err(crate::PyError::type_error(
"Initialization arguments are not supported",
));
}
Ok(obj)
}

Expand Down Expand Up @@ -1260,7 +1272,10 @@ fn stack_size(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
}
let old = STACK_SIZE.load(Ordering::Relaxed);
if let Some(&arg) = args.first() {
let size = unsafe { w_int_get_value(arg) };
// `@unwrap_spec(size=int)` (os_thread.py:216) unwraps through
// `space.int_w`, which rejects a non-integer instead of reading its
// payload word as one.
let size = crate::baseobjspace::int_w(arg)?;
if size < 0 || (size != 0 && size < 32_768) {
return Err(crate::PyError::value_error(format!(
"size not valid: {size} bytes"
Expand Down
18 changes: 14 additions & 4 deletions pyre/pyre-interpreter/src/module/time/interp_time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,17 @@ pub fn sleep(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
return Ok(w_none());
}
let dur = std::time::Duration::from_nanos(timeout_ns as u64);
// interp_time.py's `time_sleep` is an `@rffi` external call. In pyre the
// blocking mutator leaves the free-threaded GC's STW RUNNING census.
let _blocking = crate::module::thread::before_external_block();
// `nanosleep` is a `releasegil=True` external (interp_time.py:504-506), so
// only the blocking call itself runs outside the free-threaded GC's STW
// RUNNING census. `checksignals` runs with the GIL re-acquired
// (interp_time.py:707), i.e. with the mutator back in the census — so the
// guard is scoped to each call, not to the whole retry loop. A Python
// signal handler running outside the census would trip the running-mutator
// assertions on its first allocation or blocking call.
#[cfg(feature = "sandbox")]
{
// The controller services the sleep; signal handling is its concern.
let _blocking = crate::module::thread::before_external_block();
crate::host_seam::ops::sleep(dur.as_secs_f64())
.map_err(|e| crate::host_seam::seam_os_err(e, ""))?;
Ok(w_none())
Expand All @@ -187,7 +192,11 @@ pub fn sleep(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
let deadline = std::time::Instant::now() + dur;
let mut remaining = dur;
loop {
match host_time::nanosleep(remaining) {
let slept = {
let _blocking = crate::module::thread::before_external_block();
host_time::nanosleep(remaining)
};
match slept {
Ok(()) => return Ok(w_none()),
Err(e) if e.raw_os_error() == Some(libc::EINTR) => {
crate::module::signal::interp_signal::checksignals_now()?;
Expand All @@ -208,6 +217,7 @@ pub fn sleep(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
}
#[cfg(not(any(all(unix, feature = "host_env"), feature = "sandbox")))]
{
let _blocking = crate::module::thread::before_external_block();
std::thread::sleep(dur);
Ok(w_none())
}
Expand Down
58 changes: 42 additions & 16 deletions pyre/pyre-interpreter/src/objspace/std/mapdict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,33 @@ static INSTANCE_LOCKS: LazyLock<Vec<ForkReentrantLock>> =
static CODE_CACHE_LOCKS: LazyLock<Vec<ForkReentrantLock>> =
LazyLock::new(|| (0..256).map(|_| ForkReentrantLock::new()).collect());

fn instance_lock_for(obj: PyObjectRef) -> &'static ReentrantMutex<()> {
INSTANCE_LOCKS[(obj as usize >> 4) & (INSTANCE_LOCKS.len() - 1)].get()
type MapDictGuard = parking_lot::lock_api::ReentrantMutexGuard<
'static,
parking_lot::RawMutex,
parking_lot::RawThreadId,
(),
>;

/// A contended stripe must not be waited on from inside the running-mutator
/// census: the owner can allocate under the stripe and request a collection,
/// which then waits for this thread while this thread waits for the stripe.
/// Leave the census around the blocking acquire, as `w_list_lock` does.
fn lock_stripe(lock: &'static ReentrantMutex<()>) -> MapDictGuard {
if let Some(guard) = lock.try_lock() {
return guard;
}
let blocked = crate::module::thread::before_external_block();
let guard = lock.lock();
drop(blocked);
guard
}

fn code_cache_lock_for(code: PyObjectRef) -> &'static ReentrantMutex<()> {
CODE_CACHE_LOCKS[(code as usize >> 4) & (CODE_CACHE_LOCKS.len() - 1)].get()
fn instance_lock(obj: PyObjectRef) -> MapDictGuard {
lock_stripe(INSTANCE_LOCKS[(obj as usize >> 4) & (INSTANCE_LOCKS.len() - 1)].get())
}

fn code_cache_lock(code: PyObjectRef) -> MapDictGuard {
lock_stripe(CODE_CACHE_LOCKS[(code as usize >> 4) & (CODE_CACHE_LOCKS.len() - 1)].get())
}

pub fn after_fork_child() {
Expand Down Expand Up @@ -384,7 +405,7 @@ pub unsafe fn instance_node_setdictvalue(
name: &Wtf8,
value: PyObjectRef,
) -> bool {
let _instance_guard = instance_lock_for(obj).lock();
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &mut *(obj as *mut pyre_object::W_ObjectObject);
let map = inst._get_mapdict_map();
Expand All @@ -409,7 +430,7 @@ pub unsafe fn instance_node_setdictvalue(
/// `obj` must be a live `W_ObjectObject` (caller guards with `is_instance`).
#[majit_macros::dont_look_inside]
pub unsafe fn instance_node_getdictvalue(obj: PyObjectRef, name: &Wtf8) -> Option<PyObjectRef> {
let _instance_guard = instance_lock_for(obj).lock();
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &mut *(obj as *mut pyre_object::W_ObjectObject);
let map = inst._get_mapdict_map();
Expand All @@ -434,7 +455,7 @@ pub unsafe fn instance_node_getdictvalue(obj: PyObjectRef, name: &Wtf8) -> Optio
/// `obj` must be a live `W_ObjectObject` (caller guards with `is_instance`).
#[majit_macros::dont_look_inside]
pub unsafe fn instance_node_deldictvalue(obj: PyObjectRef, name: &Wtf8) -> bool {
let _instance_guard = instance_lock_for(obj).lock();
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &mut *(obj as *mut pyre_object::W_ObjectObject);
let map = inst._get_mapdict_map();
Expand All @@ -460,7 +481,7 @@ pub unsafe fn instance_node_deldictvalue(obj: PyObjectRef, name: &Wtf8) -> bool
/// `obj` must be a live `W_ObjectObject` (caller guards with `is_instance`).
#[majit_macros::dont_look_inside]
pub unsafe fn instance_get_dict_slot(obj: PyObjectRef) -> Option<PyObjectRef> {
let _instance_guard = instance_lock_for(obj).lock();
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &*(obj as *const pyre_object::W_ObjectObject);
let map = inst._get_mapdict_map();
Expand All @@ -480,7 +501,7 @@ pub unsafe fn instance_get_dict_slot(obj: PyObjectRef) -> Option<PyObjectRef> {
/// `obj` must be a live `W_ObjectObject` (caller guards with `is_instance`).
#[majit_macros::dont_look_inside]
pub unsafe fn instance_set_dict_slot(obj: PyObjectRef, w_dict: PyObjectRef) -> bool {
let _instance_guard = instance_lock_for(obj).lock();
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &mut *(obj as *mut pyre_object::W_ObjectObject);
let map = inst._get_mapdict_map();
Expand All @@ -494,7 +515,7 @@ pub unsafe fn instance_set_dict_slot(obj: PyObjectRef, w_dict: PyObjectRef) -> b
/// `obj` must be a live `W_ObjectObject`.
#[majit_macros::dont_look_inside]
pub unsafe fn instance_get_weakref_slot(obj: PyObjectRef) -> Option<PyObjectRef> {
let _instance_guard = instance_lock_for(obj).lock();
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &*(obj as *const pyre_object::W_ObjectObject);
let map = inst._get_mapdict_map();
Expand All @@ -511,7 +532,7 @@ pub unsafe fn instance_get_weakref_slot(obj: PyObjectRef) -> Option<PyObjectRef>
/// `obj` must be a live `W_ObjectObject`.
#[majit_macros::dont_look_inside]
pub unsafe fn instance_set_weakref_slot(obj: PyObjectRef, lifeline: PyObjectRef) -> bool {
let _instance_guard = instance_lock_for(obj).lock();
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &mut *(obj as *mut pyre_object::W_ObjectObject);
let map = inst._get_mapdict_map();
Expand All @@ -525,7 +546,7 @@ pub unsafe fn instance_set_weakref_slot(obj: PyObjectRef, lifeline: PyObjectRef)
/// `obj` must be a live `W_ObjectObject`.
#[majit_macros::dont_look_inside]
pub unsafe fn instance_del_weakref_slot(obj: PyObjectRef) {
let _instance_guard = instance_lock_for(obj).lock();
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &mut *(obj as *mut pyre_object::W_ObjectObject);
let map = inst._get_mapdict_map();
Expand Down Expand Up @@ -1193,8 +1214,8 @@ pub unsafe fn load_attr_caching(
// `_mapdict_caches` entry one atomic observation. Preserve those exact
// owners under free threading with narrow synchronization around the
// upstream cache operation.
let _instance_guard = instance_lock_for(w_obj).lock();
let _code_cache_guard = code_cache_lock_for(pycode).lock();
let _instance_guard = instance_lock(w_obj);
let _code_cache_guard = code_cache_lock(pycode);
let entry = unsafe { crate::pycode::w_code_mapdict_caches_get(pycode, nameindex) };
// mapdict.py:1482 `map = w_obj._get_mapdict_map()`.
let map = unsafe { mapdict_map_or_null(w_obj) };
Expand Down Expand Up @@ -1597,8 +1618,8 @@ pub unsafe fn store_attr_caching(
name: &str,
w_value: PyObjectRef,
) -> Result<(), PyError> {
let _instance_guard = instance_lock_for(w_obj).lock();
let _code_cache_guard = code_cache_lock_for(pycode).lock();
let _instance_guard = instance_lock(w_obj);
let _code_cache_guard = code_cache_lock(pycode);
let entry = unsafe { crate::pycode::w_code_mapdict_caches_get(pycode, nameindex) };
// mapdict.py:1577 `map = w_obj._get_mapdict_map()`.
let map = unsafe { mapdict_map_or_null(w_obj) };
Expand Down Expand Up @@ -2943,6 +2964,7 @@ static MAPDICT_ROOT_AREA: MapdictRootArea = MapdictRootArea;
/// `obj` must be a live `W_ObjectObject` backing a hasdict instance.
#[majit_macros::dont_look_inside]
pub unsafe fn instance_node_dict_length(obj: PyObjectRef) -> usize {
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &*(obj as *const pyre_object::W_ObjectObject);
let mut res: usize = 0;
Expand All @@ -2964,6 +2986,7 @@ pub unsafe fn instance_node_dict_length(obj: PyObjectRef) -> usize {
/// `obj` must be a live `W_ObjectObject` backing a hasdict instance.
#[majit_macros::dont_look_inside]
pub unsafe fn instance_node_dict_clear(obj: PyObjectRef) {
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &mut *(obj as *mut pyre_object::W_ObjectObject);
let map = inst._get_mapdict_map();
Expand Down Expand Up @@ -3002,6 +3025,7 @@ unsafe fn dict_nodes_in_order(inst: &pyre_object::W_ObjectObject) -> Vec<MapRef>
/// `obj` must be a live `W_ObjectObject` backing a hasdict instance.
#[majit_macros::dont_look_inside]
pub unsafe fn instance_node_dict_keys(obj: PyObjectRef) -> Vec<PyObjectRef> {
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &*(obj as *const pyre_object::W_ObjectObject);
let nodes = dict_nodes_in_order(inst);
Expand Down Expand Up @@ -3034,6 +3058,7 @@ pub unsafe fn instance_node_dict_keys(obj: PyObjectRef) -> Vec<PyObjectRef> {
/// `obj` must be a live `W_ObjectObject` backing a hasdict instance.
#[majit_macros::dont_look_inside]
pub unsafe fn instance_node_dict_values(obj: PyObjectRef) -> Vec<PyObjectRef> {
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &*(obj as *const pyre_object::W_ObjectObject);
let nodes = dict_nodes_in_order(inst);
Expand All @@ -3057,6 +3082,7 @@ pub unsafe fn instance_node_dict_values(obj: PyObjectRef) -> Vec<PyObjectRef> {
/// `obj` must be a live `W_ObjectObject` backing a hasdict instance.
#[majit_macros::dont_look_inside]
pub unsafe fn instance_node_dict_items(obj: PyObjectRef) -> Vec<(PyObjectRef, PyObjectRef)> {
let _instance_guard = instance_lock(obj);
ensure_mapdict_initialized(obj);
let inst = &*(obj as *const pyre_object::W_ObjectObject);
let nodes = dict_nodes_in_order(inst);
Expand Down
Loading
Loading