Skip to content
Merged
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
65 changes: 53 additions & 12 deletions pyre/pyre-jit-trace/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3495,6 +3495,30 @@ fn live_frame_array_values(
.collect()
}

/// Write one decoded Ref back into the live frame's locals_cells_stack_w
/// (the GC-rooted virtualizable array), re-asserting the resume-decoded
/// vable image after the guard-failure vsd correction cleared root slots
/// in callee coordinates. A no-op for a null frame/array, an out-of-range
/// slot, or a non-Ref value.
fn store_live_frame_array_slot(vable_ptr: usize, slot: usize, value: majit_ir::Value) {
let majit_ir::Value::Ref(r) = value else {
return;
};
if vable_ptr == 0 {
return;
}
let f = unsafe { &*(vable_ptr as *const pyre_interpreter::pyframe::PyFrame) };
let lp = f.locals_cells_stack_w;
if lp.is_null() {
return;
}
let arr = unsafe { &mut *lp };
if slot >= arr.len() {
return;
}
arr.as_mut_slice()[slot] = r.as_usize() as pyre_object::PyObjectRef;
}

/// pyframe.py:107-110: `locals_cells_stack_w` length =
/// `co_nlocals + ncellvars + nfreevars + co_stacksize`. Returns the
/// full heap-side array length (matching `virtualizable.py:86-99
Expand Down Expand Up @@ -8333,6 +8357,13 @@ impl JitState for PyreJitState {
backend,
&mut virtuals_cache,
);
if idx >= vable_array_start {
store_live_frame_array_slot(
sym.concrete_vable_ptr as usize,
idx - vable_array_start,
val,
);
Comment on lines +8361 to +8365

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve pending fields when reasserting vable virtuals

When a virtualizable_values array slot decodes as a virtual that also has deferred pending field writes, this overwrites the live frame slot with the object materialized by setup_bridge_sym. The guard-recovery path has already applied all pending fields before syncing the vable (pyre/pyre-jit/src/eval.rs:8231), but this bridge-side materializer does not replay generic rd_pendingfields (it only special-cases current-exception fields in seed_bridge_pending_current_exception), so the overwrite replaces the correct object with a stale copy missing those lazy field/array writes and the bridge can trace from corrupted object state.

Useful? React with 👍 / 👎.

}
oprefs.push(op);
concrete_values.push(val);
}
Expand Down Expand Up @@ -8596,9 +8627,23 @@ impl JitState for PyreJitState {
// array) so the seeded bridge walk can fold a branch
// derived from it without risking a moved-pointer
// stamp (gap-10 bridge sub-class; see seed note above).
// Skip a NULL (`GcRef(0)`) source, matching the
// deferred-overlay seed below: a loop-carried local
// held in a register at an interior guard reads NULL
// from `locals_cells_stack_w` because it was never
// written back. Stamping that hole poisons the real
// vable box with concrete NULL, folding a later
// residual's Ref arg to NULL →
// `MayForceNullRefArgUnsupported`. Leaving the box
// unstamped keeps it symbolic so the residual reads
// the runtime value.
if seed_bridge_locals {
if let Some(&cv) = live_local_values.get(s) {
if !matches!(cv, majit_ir::Value::Void) {
if !matches!(
cv,
majit_ir::Value::Void
| majit_ir::Value::Ref(majit_ir::GcRef(0))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve genuine PY_NULL local stamps

When a bridge local is genuinely unbound (for example after DELETE_FAST or a branch that skips assignment), the live frame slot is also PY_NULL; this file documents that frame-slot NULL means an uninitialized local, and the LOAD_FAST walker raises UnboundLocalError only when ctx.box_value(value) is Ref(PY_NULL) (trace_opcode.rs:7397-7403). This blanket skip leaves a non-constant local box unstamped in that case, so the bridge records a normal GuardNonnull/symbolic path instead of reproducing the interpreter's unbound-local exception, causing a miscompiled or perpetually deopting bridge for deleted/unassigned locals. Please distinguish stale frame-array holes from real unbound locals before dropping the NULL stamp.

Useful? React with 👍 / 👎.

) {
ctx.try_set_opref_concrete(v, cv);
}
}
Expand Down Expand Up @@ -9143,17 +9188,13 @@ impl JitState for PyreJitState {
}
}

// pyjitpl.py:3443 `synchronize_virtualizable()` inside
// `rebuild_state_after_failure` (pyjitpl.py:3454) writes
// `virtualizable_boxes` to the heap via `write_boxes()`
// (virtualizable.py:101-113). This is the ONLY call in
// RPython's bridge-resume path — there is no second call.
//
// In pyre, the equivalent write is `sync_virtualizable_after_
// guard_failure` (eval.rs:5709, `ResumeVableMode::
// GuardFailureSync`) which runs in the compiled bridge's
// guard-failure recovery chain BEFORE `setup_bridge_sym`.
// No additional synchronize call is needed here.
// `sync_virtualizable_after_guard_failure` runs before bridge setup,
// but on the multi-frame inlined-callee path its resume-decoded array
// image is then clobbered by the vsd-correction `clear_stack_above`
// (eval.rs:7766-7774), which applies a callee-coordinate depth to the
// root frame before `setup_bridge_sym` reads it. The per-item write in
// the vvals loop above re-asserts that array image immediately after
// each decode; the statics retain the deliberate PR#569 override.

// Multi-frame bridge. The body above reconstructed
// the portal (`frames[0]`) into the caller-visible root `sym`. When
Expand Down
Loading