Skip to content
Merged
309 changes: 225 additions & 84 deletions majit/majit-backend-wasm/src/codegen.rs

Large diffs are not rendered by default.

18 changes: 12 additions & 6 deletions pyre/pyre-interpreter/src/baseobjspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4921,12 +4921,12 @@ fn getdictvalue(obj: PyObjectRef, name: &str) -> Result<Option<PyObjectRef>, PyE
// `("dict", SPECIAL)` wrapper and change the instance's map — see
// [`setdictvalue`].
if unsafe { crate::objspace::std::mapdict::has_mapdict_storage(obj) } {
return Ok(unsafe {
crate::objspace::std::mapdict::instance_node_getdictvalue(
return unsafe {
crate::objspace::std::mapdict::instance_node_getdictvalue_checked(
obj,
rustpython_wtf8::Wtf8::new(name),
)
});
};
}
let w_dict = getdict_backing(obj)?;
if w_dict.is_null() {
Expand Down Expand Up @@ -5692,8 +5692,11 @@ fn getattr_str_impl(obj: PyObjectRef, name: &str, call_getattr: bool, suppress:
// same `instance_node_getdictvalue`, so the value is identical and the
// `__dict__` wrapper is built only on explicit `__dict__` access.
let value = unsafe {
crate::objspace::std::mapdict::instance_node_getdictvalue(obj, Wtf8::new(name))
};
crate::objspace::std::mapdict::instance_node_getdictvalue_checked(
obj,
Wtf8::new(name),
)
}?;
if let Some(value) = value {
return Ok(value);
}
Expand Down Expand Up @@ -6369,7 +6372,10 @@ pub fn object_getattribute(obj: PyObjectRef, name: &str) -> PyResult {
// mapdict.py:846-847); a type receiver uses only its canonical
// dictionary, which is the corresponding `getdictvalue` result.
let value = if instance {
crate::objspace::std::mapdict::instance_node_getdictvalue(obj, Wtf8::new(name))
crate::objspace::std::mapdict::instance_node_getdictvalue_checked(
obj,
Wtf8::new(name),
)?
} else {
crate::type_dict_lookup(obj, name)
};
Expand Down
25 changes: 22 additions & 3 deletions pyre/pyre-interpreter/src/module/signal/interp_signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,10 +623,19 @@ pub fn register_module(ns: pyre_object::PyObjectRef) {
#[cfg(feature = "host_env")]
{
let signum = if let Some(&a) = args.first() {
unsafe { pyre_object::w_int_get_value(a) as i32 }
unsafe { pyre_object::w_int_get_value(a) }
} else {
return Err(crate::PyError::type_error("strsignal() missing argument"));
};
// interp_signal.py:593-594 spells this bound inline rather
// than calling `check_signum_in_range`, and its `signalnum
// > NSIG` admits `NSIG` itself. 3.14 rejects it — its own
// `pthread_sigmask` reports the range as `[1; NSIG - 1]` —
// so take the half-open bound the other entry points use:
// `strsignal(NSIG)` is `ValueError` here and
// `'Unknown signal: 32'` on pypy.
check_signum_in_range(signum)?;
let signum = signum as i32;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return Ok(rustpython_host_env::signal::strsignal(signum)
.map(|s| pyre_object::w_str_new(&s))
.unwrap_or(pyre_object::w_none()));
Expand Down Expand Up @@ -855,7 +864,12 @@ pub fn register_module(ns: pyre_object::PyObjectRef) {
"siginterrupt() requires 2 arguments",
));
}
let sig = (unsafe { pyre_object::w_int_get_value(args[0]) }) as i32;
// interp_signal.py:388 — `check_signum_in_range` runs
// before the argument reaches `c_siginterrupt`, so the
// narrowing below is exact.
let sig = unsafe { pyre_object::w_int_get_value(args[0]) };
check_signum_in_range(sig)?;
let sig = sig as i32;
let flag = (unsafe { pyre_object::w_int_get_value(args[1]) }) as i32;
rustpython_host_env::signal::siginterrupt(sig, flag).map_err(|e| {
crate::PyError::os_error_with_errno(
Expand Down Expand Up @@ -1077,7 +1091,12 @@ pub fn register_module(ns: pyre_object::PyObjectRef) {
)
})?;
for it in items {
let signum = (unsafe { pyre_object::w_int_get_value(it) }) as i32;
// interp_signal.py:492 — `SignalMask.__enter__` is
// shared with `sigwait` and range-checks every
// element before `c_sigaddset`.
let signum = unsafe { pyre_object::w_int_get_value(it) };
check_signum_in_range(signum)?;
let signum = signum as i32;
rustpython_host_env::signal::sigaddset(&mut set, signum).map_err(
|e| {
crate::PyError::os_error_with_errno(
Expand Down
71 changes: 65 additions & 6 deletions pyre/pyre-interpreter/src/objspace/std/mapdict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,11 +666,31 @@ pub unsafe fn map_is_devolved(map: MapRef) -> bool {
/// `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> {
unsafe { instance_node_getdictvalue_checked(obj, name) }.unwrap_or(None)
}

/// Fallible [`instance_node_getdictvalue`], for the callers that have an error
/// channel to propagate a raising `__eq__` on.
///
/// Only the devolved terminator's dict probe can raise; the swallowing
/// spelling above is written in terms of this one and its `unwrap_or` consumes
/// the pending error slot, so a dropped error cannot surface on a later
/// operation.
///
/// `dont_look_inside` for the same reason as [`instance_node_getdictvalue`].
///
/// # Safety
/// `obj` must be a live `W_ObjectObject` (caller guards with `is_instance`).
#[majit_macros::dont_look_inside]
pub unsafe fn instance_node_getdictvalue_checked(
obj: PyObjectRef,
name: &Wtf8,
) -> Result<Option<PyObjectRef>, PyError> {
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();
let w_res = unsafe { node_read(map, inst, name, DICT) };
let w_res = unsafe { node_read_checked(map, inst, name, DICT) };
// mapdict.py:846-847 getdictvalue → read → _direct_read (592-598): lazily
// migrate to boxed storage when the read attribute is unboxed and its class
// has frozen unboxing.
Expand Down Expand Up @@ -3092,18 +3112,43 @@ unsafe fn terminator_read<O: MapdictObject>(
name: &Wtf8,
attrkind: u16,
) -> Option<PyObjectRef> {
unsafe { terminator_read_checked(term, obj, name, attrkind) }.unwrap_or(None)
}

/// Fallible [`terminator_read`]. The devolved arm is the only one that can
/// raise, and `space.finditem_str` propagates there upstream; the swallowing
/// spelling above exists for the callers that have no error channel and is
/// written in terms of this one. Its `unwrap_or` also consumes the pending
/// error slot, so a dropped error cannot surface on a later operation.
///
/// # Safety
/// `term` must point to a live Terminator map node.
unsafe fn terminator_read_checked<O: MapdictObject>(
term: MapRef,
obj: &O,
name: &Wtf8,
attrkind: u16,
) -> Result<Option<PyObjectRef>, PyError> {
let t = unsafe { (*term).as_terminator() };
match t.kind {
TerminatorKind::Devolved if attrkind == DICT => {
// mapdict.py:383-388: the devolved terminator reads DICT attributes
// from the materialised instance dict (`space.finditem_str(
// obj.getdict(space), name)`).
// obj.getdict(space), name)`). `finditem_str` is fallible: the
// probe compares against whatever the bucket holds, so a stored
// non-string key whose hash collides can reach a user `__eq__`
// that raises, and that must not read back as a miss.
let w_dict = obj.getdict();
let backing = crate::type_methods::resolve_dict_backing(w_dict);
unsafe { pyre_object::w_dict_getitem_wtf8(backing, name) }
unsafe { pyre_object::dictmultiobject::w_dict_getitem_wtf8_checked(backing, name) }
.map_err(|_| {
crate::baseobjspace::take_pending_dict_key_error(pyre_object::w_str_from_wtf8(
name.to_wtf8_buf(),
))
})
}
// Terminator / DictTerminator / NoDictTerminator read nothing.
_ => None,
_ => Ok(None),
}
}

Expand All @@ -3117,14 +3162,28 @@ pub unsafe fn node_read<O: MapdictObject>(
name: &Wtf8,
attrkind: u16,
) -> Option<PyObjectRef> {
unsafe { node_read_checked(self_node, obj, name, attrkind) }.unwrap_or(None)
}

/// Fallible [`node_read`], for the callers that can propagate the raising
/// `__eq__` a devolved terminator's dict probe may reach.
///
/// # Safety
/// `self_node` and its chain must point to live map nodes.
pub unsafe fn node_read_checked<O: MapdictObject>(
self_node: MapRef,
obj: &O,
name: &Wtf8,
attrkind: u16,
) -> Result<Option<PyObjectRef>, PyError> {
match unsafe { find_map_attr(self_node, name, attrkind) } {
// The `jit.isconstant(attr) and jit.isconstant(obj) and not
// attr.ever_mutated` guard selects `_pure_direct_read`
// (mapdict.py:60-65). The PlainAttribute variants have the same body;
// UnboxedPlainAttribute._direct_read's conversion tail lives in
// `maybe_migrate_to_boxed`.
Some(attr) => Some(unsafe { plain_direct_read(attr, obj) }),
None => unsafe { terminator_read((*self_node).terminator(), obj, name, attrkind) },
Some(attr) => Ok(Some(unsafe { plain_direct_read(attr, obj) })),
None => unsafe { terminator_read_checked((*self_node).terminator(), obj, name, attrkind) },
}
}

Expand Down
108 changes: 93 additions & 15 deletions pyre/pyre-interpreter/src/pyframe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1389,6 +1389,12 @@ impl Default for FrameDebugData {
}
}

/// Byte offset of `w_locals` in `FrameDebugData`.
pub const FRAME_DEBUG_DATA_W_LOCALS_OFFSET: usize = std::mem::offset_of!(FrameDebugData, w_locals);

/// Allocated size of a `FrameDebugData`.
pub const FRAME_DEBUG_DATA_SIZE: usize = std::mem::size_of::<FrameDebugData>();

/// pyopcode.py:1875-1897 FrameBlock — linked list node for the block stack.
/// `previous` forms a singly-linked list; `lastblock` in PyFrame is the head.
/// It is assigned only during construction and targets a strictly older node,
Expand Down Expand Up @@ -4413,29 +4419,32 @@ fn delitem_str_object(w_obj: PyObjectRef, name: &str) -> Result<(), crate::PyErr
}
}

/// `pyframe.py:557 self.space.newdict(instance=True)` — the mapping half of
/// `fast2locals`, for a trace that models the fastlocals reads instead of
/// residualizing `interp_inspect.py:7-11 locals`.
/// `pyframe.py:557 self.space.newdict(instance=True)` — the mapping
/// `fast2locals` materialises for a frame that has none yet, for a trace that
/// models the fastlocals reads instead of residualizing
/// `interp_inspect.py:7-11 locals`.
///
/// Takes no `PyFrame`: the modelled expansion feeds the slot values in as
/// ordinary Ref operands, so nothing reachable from here can call
/// [`crate::executioncontext::force_frame`]. That is the whole point of the
/// split — a helper that touched the frame would re-arm the escape this
/// modelling exists to remove. The frame's own `w_locals` cache is
/// deliberately NOT populated: an OPTIMIZED frame hands out an independent
/// copy per read (`frame_locals_snapshot`), so the cache is never the object
/// application code sees, and a later `f_locals` rebuilds it from the
/// fastlocals anyway.
/// Takes no `PyFrame`, so nothing reachable from here can call
/// [`crate::executioncontext::force_frame`] — a helper that touched the frame
/// would re-arm the escape this modelling exists to remove. The store back
/// into `debugdata.w_locals` is therefore NOT modelled either: the caller
/// pins the frame's absent mapping with a guard and hands this dict out
/// directly, which for an OPTIMIZED frame is already the independent copy
/// `frame_locals_snapshot` returns.
pub extern "C" fn jit_locals_dict_new() -> i64 {
unsafe { pyre_object::w_dict_new() as i64 }
}

/// `pyframe.py:566-568 fast2locals` for ONE visible fastlocal slot: bind
/// `code.varnames[index]` to `value` in `dict`.
///
/// Unbound slots are not routed here at all — the modelled expansion emits a
/// `guard_isnull` for them and skips the store, which is `fast2locals`'
/// `delitem` arm applied to a mapping that never held the key.
/// `dict` is the frame's own locals mapping — `getorcreatedebug().w_locals`,
/// which the modelled expansion reads through the `debugdata` virtualizable
/// field and hands in as an ordinary Ref operand. No `PyFrame` reaches this
/// helper, so nothing under it can call
/// [`crate::executioncontext::force_frame`]; that is the whole point of the
/// split, since a helper that touched the frame would re-arm the escape the
/// modelling exists to remove.
///
/// Returns `dict` so the unrolled slot chain threads the (possibly forwarded)
/// mapping from one store to the next instead of holding a raw address across
Expand Down Expand Up @@ -4467,6 +4476,75 @@ pub extern "C" fn jit_locals_dict_setitem_local(
pyre_object::gc_roots::shadow_stack_get(dict_slot) as i64
}

/// `pyframe.py:569-574 fast2locals` for ONE visible fastlocal slot that is
/// unbound: remove `code.varnames[index]` from `dict`.
///
/// The delete is fallible for the same reason
/// [`crate::baseobjspace::delitem`] is — a stored key whose hash collides
/// with the varname can reach a user `__eq__` that raises — so it routes
/// through the checked spelling. A missing key is not an error here
/// (`w_dict_delitem_checked` reports it as `Ok(false)`), which is the
/// `KeyError` arm `delitem_str_object` swallows. Anything else is reported
/// as `PY_NULL` and the pending slot is drained, so the guarded side exit
/// re-runs the residual and raises from the eval loop.
///
/// Returns `dict` on success, for the same threading reason as
/// [`jit_locals_dict_setitem_local`].
///
/// # Safety
/// `dict` must be a live dict and `code` a live `CodeObject` with
/// `index < varnames.len()`.
pub extern "C" fn jit_locals_dict_delitem_local(dict: i64, code: i64, index: i64) -> i64 {
let _roots = pyre_object::gc_roots::push_roots();
let dict_slot = pyre_object::gc_roots::shadow_stack_len();
pyre_object::gc_roots::pin_root(dict as PyObjectRef);
let code = unsafe { &*(code as usize as *const CodeObject) };
let name: &str = &code.varnames[index as usize];
let key_slot = pyre_object::gc_roots::shadow_stack_len();
pyre_object::gc_roots::pin_root(unsafe { pyre_object::w_str_new(name) });
let deleted = unsafe {
pyre_object::dictmultiobject::w_dict_delitem_checked(
pyre_object::gc_roots::shadow_stack_get(dict_slot),
pyre_object::gc_roots::shadow_stack_get(key_slot),
)
};
match deleted {
Ok(_) => pyre_object::gc_roots::shadow_stack_get(dict_slot) as i64,
Err(_) => {
let _ = crate::baseobjspace::take_pending_dict_key_error(
pyre_object::gc_roots::shadow_stack_get(key_slot),
);
pyre_object::PY_NULL as i64
}
}
}

/// The copy half of [`PyFrame::frame_locals_snapshot`]: an INDEPENDENT dict
/// holding what the frame's own locals mapping holds (PEP 667), which is what
/// `locals()` / `vars()` hand back for an OPTIMIZED frame.
///
/// Reports a failing copy as `PY_NULL` rather than publishing it, so the
/// guarded side exit re-runs the residual and raises from the eval loop.
///
/// # Safety
/// `w_locals` must be a live mapping.
pub extern "C" fn jit_locals_dict_snapshot(w_locals: i64) -> i64 {
let _roots = pyre_object::gc_roots::push_roots();
let locals_slot = pyre_object::gc_roots::shadow_stack_len();
pyre_object::gc_roots::pin_root(w_locals as PyObjectRef);
let snapshot_slot = pyre_object::gc_roots::shadow_stack_len();
pyre_object::gc_roots::pin_root(unsafe { pyre_object::w_dict_new() });
// `dict_update_value` walks a mapping's `keys()`, so both sides are
// reloaded across it as well as across the `w_dict_new` above.
match crate::opcode_ops::dict_update_value(
pyre_object::gc_roots::shadow_stack_get(snapshot_slot),
pyre_object::gc_roots::shadow_stack_get(locals_slot),
) {
Ok(()) => pyre_object::gc_roots::shadow_stack_get(snapshot_slot) as i64,
Err(_) => pyre_object::PY_NULL as i64,
}
}

#[cfg(test)]
mod tests {
use super::load_const_from_code;
Expand Down
32 changes: 32 additions & 0 deletions pyre/pyre-jit-trace/src/descr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1773,6 +1773,32 @@ static RBIGINT_PAIR_DESCR_GROUP: LazyLock<PyreObjectDescrGroup> = LazyLock::new(
)
});

// `pyframe.py:44 FrameDebugData.w_locals` — the frame's own locals mapping,
// reached as `self.getorcreatedebug().w_locals` at the head of `fast2locals`
// (pyframe.py:555-557). Not a PyObject — no vtable and no allocation type id,
// because the trace never NEWs one: it arrives as the `debugdata`
// virtualizable field and is only read. The field is MUTABLE (`setdictscope`
// and `fast2locals`' lazy materialisation both rebind it), so the read must
// not be treated as always-pure.
static FRAME_DEBUG_DATA_DESCR_GROUP: LazyLock<PyreObjectDescrGroup> = LazyLock::new(|| {
build_object_descr_group_with_def_path(
pyre_interpreter::pyframe::FRAME_DEBUG_DATA_SIZE,
0,
0,
&[(
"w_locals",
pyre_interpreter::pyframe::FRAME_DEBUG_DATA_W_LOCALS_OFFSET,
std::mem::size_of::<usize>(),
Type::Ref,
false,
false,
false,
)],
"FrameDebugData",
"pyframe::FrameDebugData",
)
});

// `pypy/objspace/std/sliceobject.py:13` `W_SliceObject._immutable_fields_ =
// ['w_start', 'w_stop', 'w_step']` — all three Ref fields are immutable
// once `__init__` runs. The `space.newslice(w_start, w_end, w_step)` JIT
Expand Down Expand Up @@ -3063,6 +3089,12 @@ pub fn rbigint_pair_item1_descr() -> DescrRef {
field_descr_from_group(&RBIGINT_PAIR_DESCR_GROUP, 1)
}

/// `FrameDebugData.w_locals` — the mapping `getorcreatedebug().w_locals`
/// reads at the head of `fast2locals` (pyframe.py:555-557).
pub fn frame_debug_data_w_locals_descr() -> DescrRef {
field_descr_from_group(&FRAME_DEBUG_DATA_DESCR_GROUP, 0)
}

pub fn str_len_descr() -> DescrRef {
// Python len(str) returns codepoint count.
// unicodeobject.py:165 W_UnicodeObject._len() → _length field.
Expand Down
Loading
Loading