From 417d10c5e4c15b1cc8854efb1f6f58d03e8b69ab Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 5 Aug 2026 01:42:56 +0900 Subject: [PATCH 1/6] jit(fbw): scan the walked frame's own jitcode in classify_compare_box_use `classify_compare_box_use` decoded `compare_pc` against the snapshot root's JitCode and bailed out with `Other` on every inlined sub-walk, since the callee's offset does not index the root's bytes. Take the bytes from the frame the walk is in (`framestack.last().w_code` -> `sub_jitcode_body_for_code`). The liveness tables condition 4 reads are keyed on the root jitcode index and do not describe a callee, so the sub-walk path reports the shape with `arms_dead: false`: `walker_newbool_guarded` uses it, and the box elision in `compare_box_provably_dead` still requires condition 4 and keeps declining inside a sub-walk. On pyre/bench/fib_recursive.py this drops 3 `CallR(newbool)` residuals whose results are read by nothing (2 of them in the hottest loop). Recorded jitstats are unchanged (loops_compiled=1 bridges_compiled=3 loops_aborted=0 guard_failures=406). Assisted-by: Claude --- .../src/jitcode_dispatch/mod.rs | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs index ffaac0f5c9d..425ee0c30a0 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs @@ -8117,34 +8117,47 @@ fn classify_compare_box_use( dst_reg: u8, publish: VablePublish, ) -> CompareBoxUse { - // `compare_pc` indexes the JitCode being walked, while everything below - // reads the snapshot root's. An inlined sub-walk has a distinct callee - // JitCode and deliberately shares the root snapshot, so the two disagree - // and the scan would decode the callee's offset against the caller's - // bytes. `walker_foriter_green_key` declines the same mismatch. - if ctx.fbw_mode.inline_subwalk { - return CompareBoxUse::Other; - } - let full_body_sym = ctx.fbw_mode.snapshot_sym; - if full_body_sym.is_null() { - return CompareBoxUse::Other; - } - // SAFETY: same contract as walker_capture_snapshot_for_last_guard_impl — - // pointer live for the full-body walk, immutable layout fields only. - let (code, jitcode_index, payload): (&[u8], i32, &crate::PyJitCode) = unsafe { - let sym = &*full_body_sym; - if sym.jitcode().is_null() { - return CompareBoxUse::Other; + // `compare_pc` indexes the JitCode being walked. For a root walk that is + // the snapshot root's; an inlined sub-walk drives the callee's OWN per-fn + // body, so decoding the callee's offset against the root's bytes reads + // unrelated ops. Take the bytes from the frame the walk is actually in — + // upstream reads the bytecode per frame as well (`MIFrame.setup`, + // `pyjitpl.py:74-80`, assigns `self.bytecode` at every `perform_call`). + // + // Condition 4's liveness tables are keyed on the ROOT jitcode index and do + // not describe a callee, so a callee scan reports the shape without it. + // That is what [`walker_newbool_guarded`] asks for; the box elision in + // [`compare_box_provably_dead`] requires condition 4 and so keeps declining + // inside a sub-walk exactly as before. + let (code, liveness): (&[u8], Option<(i32, &crate::PyJitCode)>) = if ctx.fbw_mode.inline_subwalk + { + let w_code = ctx.session.borrow().framestack.last().map(|f| f.w_code); + match w_code.and_then(|c| crate::state::sub_jitcode_body_for_code(c as *const ())) { + Some(body) => (body.code, None), + // An inline level with no resolvable body: no bytes to scan. + None => return CompareBoxUse::Other, } - let jc = &*sym.jitcode(); - if jc.payload.code_ptr.is_null() { + } else { + let full_body_sym = ctx.fbw_mode.snapshot_sym; + if full_body_sym.is_null() { return CompareBoxUse::Other; } - ( - jc.payload.jitcode.code.as_slice(), - jc.index as i32, - &jc.payload, - ) + // SAFETY: same contract as walker_capture_snapshot_for_last_guard_impl — + // pointer live for the full-body walk, immutable layout fields only. + unsafe { + let sym = &*full_body_sym; + if sym.jitcode().is_null() { + return CompareBoxUse::Other; + } + let jc = &*sym.jitcode(); + if jc.payload.code_ptr.is_null() { + return CompareBoxUse::Other; + } + ( + jc.payload.jitcode.code.as_slice(), + Some((jc.index as i32, &jc.payload)), + ) + } }; let Some(start) = crate::jitcode_runtime::decode_op_at(code, compare_pc) else { return CompareBoxUse::Other; @@ -8226,6 +8239,11 @@ fn classify_compare_box_use( let Some(gin_op) = crate::jitcode_runtime::decode_op_at(code, gin_pc) else { return CompareBoxUse::Other; }; + // A callee scan carries no liveness table of its own (see above), so it + // reports the shape alone and leaves condition 4 unproven. + let Some((jitcode_index, payload)) = liveness else { + return CompareBoxUse::FeedsBranchOnly { arms_dead: false }; + }; // Condition 4: `dst_reg`'s color must be dead at BOTH branch arms (the // POP_JUMP pops the tested bool regardless of direction, so the // guard's resume — whichever arm is not-taken — must not carry it). From cf6411af7d171869d099a02a58a4b4a51c3d22fc Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 5 Aug 2026 09:02:52 +0900 Subject: [PATCH 2/6] jit: thread the execution context through the CALL residual instead of the caller frame MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bh_call_fn_impl` resolved a caller frame with `getexecutioncontext().gettopframe_raw()`, which is `force_vref`. While the trace records, that chain slot holds a vref stamped `TOKEN_TRACING_RESCALL` by `tracing_before_residual_call`; `force_virtual` clears the token (`virtualref.py:161-167`) and `tracing_after_residual_call` reads the cleared token as "the callee forced this vref", so `vrefs_after_residual_call` -> `stop_tracking_virtualref` recorded the non-NULL `VIRTUAL_REF_FINISH` ahead of the call. The helper's own read was the escape. `bhimpl_residual_call_r_r` (blackhole.py:1227) is `cpu.bh_call_r(func, None, args_r, ...)` and carries no frame; the callee frame takes its execution context from the space. Pass the execution context: * `prepare_user_call` takes `execution_context`, the only field it read off the caller frame. * `call_user_function_residual` is replaced by `call_user_function_residual_with_ctx` — `call_user_function_plain_with_ctx` with the JIT-aware eval function. The caller-side `FrameLocalsRoot` goes with the caller frame. * `create_callee_frame_in_ctx` splits out of `create_callee_frame_impl`, which read the caller only for `execution_context`. * the self-recursion probe reads `vref_referent`, the non-forcing chain read documented for identity tests, and declines on a null referent. * `set_last_exec_ctx` takes the `ec` already in hand on the user-function and cold paths. * the `PYRE_BH_NULL_ARG` diagnostic resolves the frame itself. `bh_call_kw_impl` and `bh_call_function_ex_fn` still call `gettopframe_raw()`: `call_kw` / `call_function_ex` reach `call_function_carrier_with_mode`, which reads `frame.get_is_being_profiled()` and hands the frame to `call_args_and_c_profile` (baseobjspace.py:1243). Measured on dynasm, one binary pair, output byte-identical on every bench. Minor collections: fib_recursive 5267 -> 4645, linrec 483 -> 405, treerec 1710 -> 1498, nbody 414 -> 414. fib_recursive child CPU 0.868x and 0.886x across the two arm orders (min of 9, interleaved). The compiled traces are unchanged: the `after opt` op counts match the baseline binary on `inline_gate_operand_provenance` (61/61/61/49) and `kept_stack_deep_var_shortcircuit` (105/131). `cargo test -p pyre-jit -p pyre-interpreter --features dynasm`: 842 passed. `check.py --backend dynasm,cranelift,wasm`: no correctness failure; dynasm 375 passed / cranelift 376 / wasm 371, with 5 jit-stats rows moving. No `.jitstats` re-recorded here. Assisted-by: Claude --- pyre/pyre-interpreter/src/call.rs | 83 ++++++++++++++--------- pyre/pyre-jit/src/call_jit.rs | 107 ++++++++++++++++++------------ 2 files changed, 116 insertions(+), 74 deletions(-) diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index 57ecbe1dc68..ffc243cb602 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -741,7 +741,7 @@ enum PreparedUserCall { /// temporaries are live while the recursive evaluator runs. #[inline(never)] fn prepare_user_call( - frame: &PyFrame, + execution_context: *const crate::PyExecutionContext, callable: PyObjectRef, args: &[PyObjectRef], ) -> Result { @@ -753,13 +753,8 @@ fn prepare_user_call( }; let code_ref = unsafe { &*func_code }; let final_args = fill_user_function_args(callable, code_ref, args)?; - let func_frame = make_user_call_frame( - w_code, - &final_args, - w_globals, - frame.execution_context, - closure, - )?; + let func_frame = + make_user_call_frame(w_code, &final_args, w_globals, execution_context, closure)?; if crate::pyframe::code_flags_make_generator(code_ref.flags) { return frame_into_generator_for_function(func_frame, callable) @@ -774,7 +769,7 @@ fn call_user_function_with_eval( args: &[PyObjectRef], eval_fn: EvalFn, ) -> PyResult { - let mut func_frame = match prepare_user_call(frame, callable, args)? { + let mut func_frame = match prepare_user_call(frame.execution_context, callable, args)? { PreparedUserCall::Frame(func_frame) => func_frame, PreparedUserCall::Generator(generator) => return Ok(generator), }; @@ -784,6 +779,52 @@ fn call_user_function_with_eval( eval_fn(&mut func_frame) } +/// Residual-call sibling of [`call_user_function_plain`] that keeps the +/// JIT-aware eval function. +/// +/// `blackhole.py:1225 bhimpl_residual_call_r_i` is `cpu.bh_call_i(func, ...)` +/// — it invokes the *translated function*, and when that function's graph +/// reaches a `jit_merge_point` (`execute_frame` does) the JIT is entered +/// normally. "Opaque to the trace" does not mean "the JIT is off inside": +/// upstream has no flag that disables it for the extent of a residual call. +/// `bhimpl_recursive_call_*` (`blackhole.py:1095-1132`) is not the only way +/// to reach the portal — it is the path the codewriter emits when the callee +/// is *statically* the portal graph. +/// +/// Re-entrant tracing is prevented where upstream prevents it, on the green +/// key: `warmstate.py:473-477` skips a hot back-edge while `JC_TRACING` is +/// set, which pyre mirrors with the `driver.is_tracing()` guard in +/// `maybe_compile_and_run`. +/// +/// The execution context is passed in rather than read off a caller frame: +/// `bhimpl_residual_call_r_r` (`blackhole.py:1227`) is +/// `cpu.bh_call_r(func, None, args_r, ...)`, carrying no frame operand, and +/// the callee frame takes its context from the space +/// (`space.getexecutioncontext()`). A residual helper that resolved the +/// caller frame instead would have to read `topframeref`, and +/// `gettopframe_raw` is `force_vref`: forcing a vref that carries +/// `TOKEN_TRACING_RESCALL` across a residual clears the token, which +/// `tracing_after_residual_call` reads back as "the callee escaped this +/// frame" (`virtualref.py:161-167`). The recorded escape then materializes +/// the caller frame on every execution of the compiled trace. +/// +/// The caller-side `FrameLocalsRoot` is skipped for the same reason it is in +/// [`call_user_function_plain_with_ctx`] — no caller `PyFrame` is available. +/// The callee root is still installed so its locals stay reachable. +pub fn call_user_function_residual_with_ctx( + execution_context: *const crate::PyExecutionContext, + callable: PyObjectRef, + args: &[PyObjectRef], +) -> PyResult { + let mut func_frame = match prepare_user_call(execution_context, callable, args)? { + PreparedUserCall::Frame(func_frame) => func_frame, + PreparedUserCall::Generator(generator) => return Ok(generator), + }; + func_frame.fix_array_ptrs(); + let _callee_locals_root = FrameLocalsRoot::new_mut(&mut func_frame); + get_eval_fn()(&mut func_frame) +} + /// Call a user function with pre-resolved args (scope already packed by /// resolve_kwargs). Skips defaults-fill and pack_varargs — the caller /// (call_kw) already produced the final scope via resolve_kwargs which @@ -1553,30 +1594,6 @@ pub fn call_user_function( call_user_function_with_eval(frame, callable, args, eval_fn) } -/// Residual-call sibling of [`call_user_function_plain`] that keeps the -/// JIT-aware eval function. -/// -/// `blackhole.py:1225 bhimpl_residual_call_r_i` is `cpu.bh_call_i(func, ...)` -/// — it invokes the *translated function*, and when that function's graph -/// reaches a `jit_merge_point` (`execute_frame` does) the JIT is entered -/// normally. "Opaque to the trace" does not mean "the JIT is off inside": -/// upstream has no flag that disables it for the extent of a residual call. -/// `bhimpl_recursive_call_*` (`blackhole.py:1095-1132`) is not the only way -/// to reach the portal — it is the path the codewriter emits when the callee -/// is *statically* the portal graph. -/// -/// Re-entrant tracing is prevented where upstream prevents it, on the green -/// key: `warmstate.py:473-477` skips a hot back-edge while `JC_TRACING` is -/// set, which pyre mirrors with the `driver.is_tracing()` guard in -/// `maybe_compile_and_run`. -pub fn call_user_function_residual( - frame: &PyFrame, - callable: PyObjectRef, - args: &[PyObjectRef], -) -> PyResult { - call_user_function_with_eval(frame, callable, args, get_eval_fn()) -} - /// Plain interpreter-only user-function call. /// /// JIT residual helpers should use this instead of the injected eval override. diff --git a/pyre/pyre-jit/src/call_jit.rs b/pyre/pyre-jit/src/call_jit.rs index ae6395e846b..ccec01e78e6 100644 --- a/pyre/pyre-jit/src/call_jit.rs +++ b/pyre/pyre-jit/src/call_jit.rs @@ -4302,14 +4302,28 @@ fn create_self_recursive_callee_frame_impl_1_boxed( } fn create_callee_frame_impl(caller_frame: i64, callable: i64, args: &[PyObjectRef]) -> i64 { - let callable = callable as PyObjectRef; - let w_code = unsafe { pyre_interpreter::getcode(callable) }; let caller = unsafe { &*(caller_frame as *const PyFrame) }; + create_callee_frame_in_ctx(caller.execution_context, callable as PyObjectRef, args) +} + +/// [`create_callee_frame_impl`] with the execution context passed directly. +/// +/// The caller frame contributes nothing else here — `function.py funccall` +/// builds the callee frame out of the *callee's* code, globals and closure, +/// and takes the execution context from the space. A residual helper that +/// already holds `space.getexecutioncontext()` therefore has no reason to +/// resolve a caller frame for this. +fn create_callee_frame_in_ctx( + execution_context: *const pyre_interpreter::PyExecutionContext, + callable: PyObjectRef, + args: &[PyObjectRef], +) -> i64 { + let w_code = unsafe { pyre_interpreter::getcode(callable) }; let w_globals = unsafe { function_get_globals_obj(callable) }; let args = fill_positional_defaults_for_jit_call(callable, w_code, args); let args = args.as_ref(); - alloc_callee_frame(w_code, args, w_globals, caller.execution_context) as i64 + alloc_callee_frame(w_code, args, w_globals, execution_context) as i64 } #[majit_macros::dont_look_inside] @@ -4513,10 +4527,25 @@ pub extern "C" fn jit_frame_set_slot_float(frame_ptr: i64, idx: i64, raw: f64) { // =========================================================================== fn bh_call_self_recursive_portal( - parent_frame_ptr: *const PyFrame, + ec: *const pyre_interpreter::PyExecutionContext, callable: PyObjectRef, args: &[PyObjectRef], ) -> Option { + // "Is the caller running this same code object?" is an identity test on + // the chain slot, which is what `vref_referent` exists for: it returns + // what `topframeref` NAMES, without forcing. `gettopframe_raw` forces, + // and a force while the trace records clears the + // `TOKEN_TRACING_RESCALL` marker (`virtualref.py:161-167`) that + // `tracing_after_residual_call` reads back as "the callee escaped this + // vref" — the probe would report its own read as an escape, and the + // recorded `VIRTUAL_REF_FINISH` then materializes the caller frame on + // every execution of the compiled trace. + // + // A null referent names no reachable frame (a caller that is still + // virtual), so there is nothing to compare: decline, and the generic + // residual below runs the call. + let parent_frame_ptr = + unsafe { pyre_interpreter::executioncontext::vref_referent((*ec).topframeref) }; if parent_frame_ptr.is_null() { return None; } @@ -4533,7 +4562,7 @@ fn bh_call_self_recursive_portal( // jitdriver's portal runner. This branch narrows pyre's generic // Python CALL helper back to that shape for self-recursive portal // calls; non-recursive residual calls below remain opaque plain calls. - let frame_ptr = create_callee_frame_impl(parent_frame_ptr as i64, callable as i64, args); + let frame_ptr = create_callee_frame_in_ctx(ec, callable, args); let result = { let frame = unsafe { &mut *(frame_ptr as *mut PyFrame) }; crate::eval::portal_runner_result(frame) @@ -4557,9 +4586,10 @@ fn bh_call_self_recursive_portal( /// Convention: residual_call_r_r dispatches with /// args=[callable, null_or_self, arg0, ..., argN]. RPython /// `bhimpl_residual_call_r_r` (blackhole.py:1227) carries no frame — -/// `cpu.bh_call_r(func, None, args_r, ...)`; `bh_call_fn_impl` resolves -/// the parent frame from the execution context's top frame instead of a -/// threaded operand. `null_or_self` is the CALL opcode's self slot +/// `cpu.bh_call_r(func, None, args_r, ...)` — and `bh_call_fn_impl` carries +/// none either: it takes `space.getexecutioncontext()` and threads that +/// through the dispatch, so no branch resolves a caller frame out of +/// `topframeref`. `null_or_self` is the CALL opcode's self slot /// (eval.rs:3216-3226): non-null means a method receiver to prepend as /// arg0, NULL means a plain call. /// @@ -4870,23 +4900,19 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO ); values }; - // `space.getexecutioncontext()` (call.rs:198 → TLS-pinned EC the eval - // loop stamps on entry) `.gettopframe_raw()` is the active caller frame — - // `executioncontext.py:85-89 enter` / `:91-109 leave` keep - // `topframeref` pointing at the running frame. A null here means the - // EC was never pinned before a residual call, which is a wiring bug, so - // fail-fast rather than corrupting the `&*frame` deref below. + // `space.getexecutioncontext()` (call.rs:449 → TLS-pinned EC the eval + // loop stamps on entry). This is the whole of the caller-side state the + // residual needs: `bhimpl_residual_call_r_r` takes no frame, and every + // frame the dispatch below builds takes its execution context from the + // space rather than from a caller frame. A null here means the EC was + // never pinned before a residual call, which is a wiring bug, so + // fail-fast rather than building callee frames against a null context. let ec = pyre_interpreter::call::getexecutioncontext(); - let parent_frame_ptr: *const PyFrame = if ec.is_null() { - std::ptr::null() - } else { - unsafe { (*ec).gettopframe_raw() as *const PyFrame } - }; assert!( - !parent_frame_ptr.is_null(), - "bh_call_fn_impl requires a live parent PyFrame from \ - getexecutioncontext().gettopframe_raw(); the eval loop must pin the \ - execution context before any residual call" + !ec.is_null(), + "bh_call_fn_impl requires a pinned execution context from \ + getexecutioncontext(); the eval loop must pin the execution context \ + before any residual call" ); if pyre_object::gc_roots::shadow_stack_get(root_base).is_null() { let mut err = pyre_interpreter::PyError::new( @@ -4903,8 +4929,14 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO // it — report the Python coordinate before that happens instead of leaving // a SIGSEGV inside the callee as the only evidence. if bh_null_arg_diag() { + // The Python coordinate comes off the caller frame, which the dispatch + // itself never resolves — force it here, inside the diagnostic, so an + // unarmed run leaves the frame virtual. + let parent_frame_ptr = unsafe { (*ec).gettopframe_raw() as *const PyFrame }; for i in 0..args.len() { - if pyre_object::gc_roots::shadow_stack_get(root_base + 2 + i).is_null() { + if !parent_frame_ptr.is_null() + && pyre_object::gc_roots::shadow_stack_get(root_base + 2 + i).is_null() + { let frame = unsafe { &*parent_frame_ptr }; // A NULL here with a live fastlocal is an unbound blackhole // register (the resume never seeded the slot); a NULL fastlocal @@ -4951,19 +4983,16 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO }; } let call_args = reload_args(); - if let Some(result) = bh_call_self_recursive_portal(parent_frame_ptr, callable, &call_args) - { + if let Some(result) = bh_call_self_recursive_portal(ec, callable, &call_args) { return result; } let saved_ctx = pyre_interpreter::call::take_last_exec_ctx(); - // `parent_frame_ptr` is guaranteed non-null by the entry - // assert; `set_last_exec_ctx` mirrors what the portal runner - // does on frame re-entry so user functions invoked from the - // residual path observe the caller's execution context. - unsafe { - pyre_interpreter::call::set_last_exec_ctx((*parent_frame_ptr).execution_context); - } - let parent_frame = unsafe { &*parent_frame_ptr }; + // `set_last_exec_ctx` mirrors what the portal runner does on frame + // re-entry so user functions invoked from the residual path observe + // the caller's execution context. That is `ec` itself: a frame's + // `execution_context` is the context it was built under, and the top + // frame of this EC was built under this EC. + pyre_interpreter::call::set_last_exec_ctx(ec); // `blackhole.py:1225 bhimpl_residual_call_*` is opaque to the TRACE, // not to the JIT: `cpu.bh_call_*(func, ...)` runs the translated // callee, and a callee whose graph reaches a `jit_merge_point` @@ -4992,7 +5021,7 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO // `drain_backend_jit_exc` names for the walker's snapshot side). let parked = park_residual_call_exception(); let result = - pyre_interpreter::call::call_user_function_residual(parent_frame, callable, &call_args); + pyre_interpreter::call::call_user_function_residual_with_ctx(ec, callable, &call_args); unpark_residual_call_exception(parked); pyre_interpreter::call::set_last_exec_ctx(saved_ctx); return match result { @@ -5004,18 +5033,14 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO }; } // Cold path: type/method/staticmethod/classmethod/callable-instance. - // Ensure LAST_EXEC_CTX reflects the caller frame before delegating to + // Ensure LAST_EXEC_CTX reflects the calling context before delegating to // `call_function_impl_result`. `type_descr_call_impl` → // `call_user_function_with_args` reads LAST_EXEC_CTX as the fallback // execution context for `__new__`/`__init__` (call.rs:1104-1106); // without this pin it would use whatever frame last entered // `eval_frame_*`, which is not guaranteed to be the blackhole caller. let saved_ctx = pyre_interpreter::call::take_last_exec_ctx(); - if !parent_frame_ptr.is_null() { - unsafe { - pyre_interpreter::call::set_last_exec_ctx((*parent_frame_ptr).execution_context); - } - } + pyre_interpreter::call::set_last_exec_ctx(ec); let _plain_guard = pyre_interpreter::call::force_plain_eval(); let callable = pyre_object::gc_roots::shadow_stack_get(root_base); let call_args = reload_args(); From 362df40e2ce8a84708b790a0e0228f1136022daf Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 5 Aug 2026 13:08:09 +0900 Subject: [PATCH 3/6] interp: separate the frame-taking call dispatcher from the frameless one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `call_valuestack` (baseobjspace.py:1243) is the only call dispatcher upstream hands a frame. `call_args` (descroperation.py:189) and `Function.call_args` (function.py:79) take none, and the C-level profile check sits at the opcode level, where `self` is the frame (pyopcode.py:1402 CALL_FUNCTION_KW, :1429 CALL_FUNCTION_EX). pyre had that profile check inside the generic dispatcher, so a caller frame had to be threaded through the whole chain. Convert `call_user_function_resolved`, `call_callable_with_mode`, `call_non_function_callable_with_mode`, `call_kw_in_ctx`, `call_function_ex_in_ctx`, `call_with_kwargs_in_ctx` and `type_descr_call_with_mode` to take `*const PyExecutionContext`, and add `c_profile_frame`, which resolves a caller frame only when a profiler is installed — `executioncontext.py:147-149 call_trace` sets `is_being_profiled` only while `profilefunc is not None`, and `:121-123 _c_call_return_trace` clears it, so testing the execution context first preserves the implication. `call_callable`, `call_kw`, `call_function_ex` and `call_with_kwargs` keep their frame-taking signatures as wrappers that install `FrameLocalsRoot` and delegate to the `_in_ctx` bodies, so no external caller changes. `bh_call_kw_impl` and `bh_call_function_ex_fn` no longer resolve a caller frame, matching `bh_call_fn_impl`. Assisted-by: Claude --- pyre/pyre-interpreter/src/call.rs | 292 ++++++++++++++++++++++-------- pyre/pyre-jit/src/call_jit.rs | 61 +++---- 2 files changed, 240 insertions(+), 113 deletions(-) diff --git a/pyre/pyre-interpreter/src/call.rs b/pyre/pyre-interpreter/src/call.rs index ffc243cb602..7490d526f6c 100644 --- a/pyre/pyre-interpreter/src/call.rs +++ b/pyre/pyre-interpreter/src/call.rs @@ -779,7 +779,11 @@ fn call_user_function_with_eval( eval_fn(&mut func_frame) } -/// Residual-call sibling of [`call_user_function_plain`] that keeps the +/// [`call_user_function`] with the execution context in place of the caller +/// frame — the `function.py:79 Function.call_args(self, args)` shape, which +/// upstream reaches with no frame at all. +/// +/// Also the residual-call sibling of [`call_user_function_plain`], keeping the /// JIT-aware eval function. /// /// `blackhole.py:1225 bhimpl_residual_call_r_i` is `cpu.bh_call_i(func, ...)` @@ -811,7 +815,7 @@ fn call_user_function_with_eval( /// The caller-side `FrameLocalsRoot` is skipped for the same reason it is in /// [`call_user_function_plain_with_ctx`] — no caller `PyFrame` is available. /// The callee root is still installed so its locals stay reachable. -pub fn call_user_function_residual_with_ctx( +pub fn call_user_function_with_ctx( execution_context: *const crate::PyExecutionContext, callable: PyObjectRef, args: &[PyObjectRef], @@ -830,7 +834,7 @@ pub fn call_user_function_residual_with_ctx( /// (call_kw) already produced the final scope via resolve_kwargs which /// mirrors PyPy's Arguments.parse_into_scope. pub fn call_user_function_resolved( - frame: &PyFrame, + execution_context: *const crate::PyExecutionContext, callable: PyObjectRef, args: &[PyObjectRef], ) -> PyResult { @@ -849,7 +853,7 @@ pub fn call_user_function_resolved( w_code, args, w_globals, - frame.execution_context, + execution_context, closure, crate::pyframe::FrameLocalsArrayAllocation::OldGenGc, )?); @@ -863,12 +867,11 @@ pub fn call_user_function_resolved( w_code, args, w_globals, - frame.execution_context, + execution_context, closure, crate::pyframe::FrameLocalsArrayAllocation::OldGenGc, )?); func_frame.fix_array_ptrs(); - let _caller_locals_root = FrameLocalsRoot::new(frame); let _callee_locals_root = FrameLocalsRoot::new_mut(&mut func_frame); eval_fn(&mut func_frame) } @@ -987,8 +990,56 @@ enum CallMode { Plain, } +/// `baseobjspace.py:1243 call_valuestack(w_func, nargs, frame, …)` — the one +/// dispatcher upstream gives a frame to. It settles the C-profile question and +/// hands off to the frameless `space.call_args`, which is +/// [`call_callable_in_ctx`]. +/// +/// Pyre adds one thing to that shape: `FrameLocalsRoot` on the caller. RPython +/// roots the caller's locals through the shadowstack of the translated +/// `call_valuestack`; this Rust ABI boundary is outside that transform, so the +/// root is installed here and held across the whole dispatch. pub fn call_callable(frame: &mut PyFrame, callable: PyObjectRef, args: &[PyObjectRef]) -> PyResult { - call_callable_with_mode(frame, callable, args, CallMode::Jit) + let _caller_locals_root = FrameLocalsRoot::new(frame); + call_callable_with_mode(frame.execution_context, callable, args, CallMode::Jit) +} + +/// `descroperation.py:189 call_args(space, w_obj, args)` — the generic callable +/// dispatcher, which upstream reaches with **no frame**: `Function.call_args` +/// (`function.py:79`) goes straight to `code.funcrun(self, args)` and the callee +/// frame takes its execution context from the space. +pub fn call_callable_in_ctx( + execution_context: *const crate::PyExecutionContext, + callable: PyObjectRef, + args: &[PyObjectRef], +) -> PyResult { + call_callable_with_mode(execution_context, callable, args, CallMode::Jit) +} + +/// The caller frame the C-level profile arm needs, or null when no profiler is +/// installed. +/// +/// `baseobjspace.py:1245` gates the arm on `frame.get_is_being_profiled()`, and +/// that flag has exactly one writer: `executioncontext.py:147-149 call_trace` +/// sets it only while `profilefunc is not None`, and `:121-123 +/// _c_call_return_trace` clears it and returns the moment `profilefunc is +/// None`. So an execution context with no profiler installed cannot take the +/// arm, and testing that first leaves the frame unresolved on the ordinary +/// path — `gettopframe_raw` is `force_vref`, and a vref forced while the trace +/// records is marked as escaping (`virtualref.py:161-167`). +fn c_profile_frame(execution_context: *const crate::PyExecutionContext) -> *mut PyFrame { + if execution_context.is_null() { + return std::ptr::null_mut(); + } + let ec = unsafe { &*execution_context }; + if ec.profilefunc.is_none() { + return std::ptr::null_mut(); + } + let frame = ec.gettopframe_raw(); + if frame.is_null() || !unsafe { (*frame).get_is_being_profiled() } { + return std::ptr::null_mut(); + } + frame } /// Function/_BuiltinFunction leaf of ObjSpace call dispatch. @@ -1000,12 +1051,11 @@ pub fn call_callable(frame: &mut PyFrame, callable: PyObjectRef, args: &[PyObjec /// the method arm below preserve that shape instead of retaining a second /// large generic-dispatch Rust frame for every Python method call. fn call_function_carrier_with_mode( - frame: &mut PyFrame, + execution_context: *const crate::PyExecutionContext, callable: PyObjectRef, args: &[PyObjectRef], mode: CallMode, ) -> PyResult { - let frame_ptr = frame as *mut PyFrame; match classify_callable(callable)? { CallableKind::Builtin => { // baseobjspace.py:1243 — `if frame.get_is_being_profiled() and @@ -1014,14 +1064,10 @@ fn call_function_carrier_with_mode( // here: `classify_callable` already selected the builtin arm // (`runtime_ops.rs`: `if is_builtin_code(code) { Builtin }`), // so reaching this closure means the callable is a builtin. - // The remaining condition is the per-frame profile flag, set - // by `ec.call_trace` (executioncontext.py:150) on frame entry - // and cleared by `_c_call_return_trace` when profilefunc was - // turned off (executioncontext.py:122-123). - let profile_active = unsafe { (*frame_ptr).get_is_being_profiled() }; - if profile_active { + let profile_frame = c_profile_frame(execution_context); + if !profile_frame.is_null() { let w_res = crate::baseobjspace::call_args_and_c_profile( - unsafe { &mut *frame_ptr }, + unsafe { &mut *profile_frame }, callable, args, ); @@ -1035,12 +1081,33 @@ fn call_function_carrier_with_mode( call_builtin_code_positional(code as pyre_object::PyObjectRef, args) } CallableKind::User => match mode { - CallMode::Jit => call_user_function(frame, callable, args), - CallMode::Plain => call_user_function_plain(frame, callable, args), + CallMode::Jit => call_user_function_with_ctx(execution_context, callable, args), + CallMode::Plain => call_user_function_plain_with_ctx(execution_context, callable, args), }, } } +/// [`call_function_ex_in_ctx`] reached from a frame — the `pyopcode.py:1429 +/// CALL_FUNCTION_EX` shape, whose else-branch is the frameless +/// `space.call_args(w_function, args)`. Installs the caller `FrameLocalsRoot` +/// the way [`call_callable`] does. +pub fn call_function_ex( + frame: &mut PyFrame, + callable: PyObjectRef, + self_or_null: PyObjectRef, + starargs: PyObjectRef, + kwargs_or_null: PyObjectRef, +) -> PyResult { + let _caller_locals_root = FrameLocalsRoot::new(frame); + call_function_ex_in_ctx( + frame.execution_context, + callable, + self_or_null, + starargs, + kwargs_or_null, + ) +} + /// CALL_FUNCTION_EX helper — unpack `starargs`, merge the `**` mapping, and /// call. Factored out of the interpreter's `call_function_ex` so the JIT /// residual (`bh_call_function_ex_fn`) shares one implementation. Mirrors @@ -1049,8 +1116,8 @@ fn call_function_carrier_with_mode( /// the iter protocol; a non-null `**` mapping accepts the dict fast path or /// `keys()`/`__getitem__`. `self_or_null` is the pre-callable stack slot — /// a non-null value prepends as arg0. -pub fn call_function_ex( - frame: &mut PyFrame, +pub fn call_function_ex_in_ctx( + execution_context: *const crate::PyExecutionContext, callable: PyObjectRef, self_or_null: PyObjectRef, starargs: PyObjectRef, @@ -1101,11 +1168,32 @@ pub fn call_function_ex( .zip(keywords_w.iter()) .map(|(&k, &v)| (unsafe { pyre_object::w_str_get_wtf8(k) }.to_owned(), v)) .collect(); - return call_with_kwargs(frame, callable, &args, &entries); + return call_with_kwargs_in_ctx(execution_context, callable, &args, &entries); } } - call_callable(frame, callable, &args) + call_callable_in_ctx(execution_context, callable, &args) +} + +/// [`call_kw_in_ctx`] reached from a frame — the `pyopcode.py:1402 +/// CALL_FUNCTION_KW` shape, whose else-branch is the frameless +/// `space.call_args(w_function, args)`. Installs the caller `FrameLocalsRoot` +/// the way [`call_callable`] does. +pub fn call_kw( + frame: &mut PyFrame, + callable: PyObjectRef, + self_or_null: PyObjectRef, + positional: &[PyObjectRef], + kwarg_names: PyObjectRef, +) -> PyResult { + let _caller_locals_root = FrameLocalsRoot::new(frame); + call_kw_in_ctx( + frame.execution_context, + callable, + self_or_null, + positional, + kwarg_names, + ) } /// CALL_KW helper — resolve keyword arguments against the callable and @@ -1115,8 +1203,8 @@ pub fn call_function_ex( /// included); `kwarg_names` is the constant kwnames tuple (its length is /// the number of trailing keyword args). `self_or_null` is the /// pre-callable stack slot — a non-null value prepends as arg0. -pub fn call_kw( - frame: &mut PyFrame, +pub fn call_kw_in_ctx( + execution_context: *const crate::PyExecutionContext, callable: PyObjectRef, self_or_null: PyObjectRef, positional: &[PyObjectRef], @@ -1163,7 +1251,12 @@ pub fn call_kw( kw_entries.push((key, args[n_pos + ki])); } } - return call_with_kwargs(frame, callable_unwrapped, &pos_args, &kw_entries); + return call_with_kwargs_in_ctx( + execution_context, + callable_unwrapped, + &pos_args, + &kw_entries, + ); } // A base classmethod has no tp_call, but a user subtype may define @@ -1185,7 +1278,12 @@ pub fn call_kw( kw_entries.push((key, args[n_pos + ki])); } } - return call_with_kwargs(frame, callable_unwrapped, &pos_args, &kw_entries); + return call_with_kwargs_in_ctx( + execution_context, + callable_unwrapped, + &pos_args, + &kw_entries, + ); } // For type objects with kwargs: use call_with_kwargs which handles @@ -1207,7 +1305,12 @@ pub fn call_kw( kw_entries.push((key, args[n_pos + ki])); } } - return call_with_kwargs(frame, callable_unwrapped, &pos_args, &kw_entries); + return call_with_kwargs_in_ctx( + execution_context, + callable_unwrapped, + &pos_args, + &kw_entries, + ); } } @@ -1231,7 +1334,12 @@ pub fn call_kw( kw_entries.push((key, args[n_pos + ki])); } } - return call_with_kwargs(frame, callable_unwrapped, &pos_args, &kw_entries); + return call_with_kwargs_in_ctx( + execution_context, + callable_unwrapped, + &pos_args, + &kw_entries, + ); } // Resolve keyword args into positional order. @@ -1269,9 +1377,14 @@ pub fn call_kw( // through call_with_kwargs so pyre's profile path constructs // Arguments::with_kw instead of treating the kwargs dict tail // as a positional firstarg. - return call_with_kwargs(frame, callable_unwrapped, &pos_args, &kw_entries); + return call_with_kwargs_in_ctx( + execution_context, + callable_unwrapped, + &pos_args, + &kw_entries, + ); } - return call_callable(frame, callable_unwrapped, &args); + return call_callable_in_ctx(execution_context, callable_unwrapped, &args); } // `descroperation.py descr_call` binds an instance's `__call__` and @@ -1299,7 +1412,12 @@ pub fn call_kw( kw_entries.push((key, args[n_pos + ki])); } } - return call_with_kwargs(frame, callable_unwrapped, &pos_args, &kw_entries); + return call_with_kwargs_in_ctx( + execution_context, + callable_unwrapped, + &pos_args, + &kw_entries, + ); } } @@ -1336,9 +1454,9 @@ pub fn call_kw( let _ = prepended; if unsafe { crate::is_function(target_func) } { - call_user_function_resolved(frame, target_func, &resolved) + call_user_function_resolved(execution_context, target_func, &resolved) } else { - call_callable(frame, target_func, &resolved) + call_callable_in_ctx(execution_context, target_func, &resolved) } } @@ -1474,7 +1592,7 @@ fn user_call_slot(callable: PyObjectRef) -> Result, /// descriptor/type dispatcher below. #[inline(always)] fn call_callable_with_mode( - frame: &mut PyFrame, + execution_context: *const crate::PyExecutionContext, callable: PyObjectRef, args: &[PyObjectRef], mode: CallMode, @@ -1483,7 +1601,7 @@ fn call_callable_with_mode( // speedhack and calls `funccall_valuestack` directly. Do not retain the // generic descriptor/type dispatcher across every Python frame. if unsafe { crate::is_function_carrier(callable) } { - return call_function_carrier_with_mode(frame, callable, args, mode); + return call_function_carrier_with_mode(execution_context, callable, args, mode); } if unsafe { pyre_object::is_method(callable) } { let func = unsafe { pyre_object::w_method_get_func(callable) }; @@ -1504,38 +1622,38 @@ fn call_callable_with_mode( // function.py `_Method.call_args` -> baseobjspace.py // `call_obj_args`: exact Function/BuiltinFunction carriers skip a // second generic callable dispatch. - return call_function_carrier_with_mode(frame, func, &call_args, mode); + return call_function_carrier_with_mode(execution_context, func, &call_args, mode); } - return call_callable_with_mode(frame, func, &call_args, mode); + return call_callable_with_mode(execution_context, func, &call_args, mode); } - call_non_function_callable_with_mode(frame, callable, args, mode) + call_non_function_callable_with_mode(execution_context, callable, args, mode) } #[inline(never)] fn call_non_function_callable_with_mode( - frame: &mut PyFrame, + execution_context: *const crate::PyExecutionContext, callable: PyObjectRef, args: &[PyObjectRef], mode: CallMode, ) -> PyResult { if unsafe { pyre_object::is_type(callable) } { if let Some(bound) = metaclass_call_override(callable) { - return call_callable_with_mode(frame, bound, args, mode); + return call_callable_with_mode(execution_context, bound, args, mode); } - return type_descr_call_with_mode(frame, callable, args, mode); + return type_descr_call_with_mode(execution_context, callable, args, mode); } // staticmethod → unwrap // PyPy: function.py StaticMethod.descr_call if unsafe { pyre_object::is_exact_type(callable, &pyre_object::function::STATICMETHOD_TYPE) } { let func = unsafe { pyre_object::w_staticmethod_get_func(callable) }; - return call_callable_with_mode(frame, func, args, mode); + return call_callable_with_mode(execution_context, func, args, mode); } if let Some(bound) = staticmethod_call_override(callable)? { - return call_callable_with_mode(frame, bound, args, mode); + return call_callable_with_mode(execution_context, bound, args, mode); } if let Some(bound) = classmethod_call_override(callable)? { - return call_callable_with_mode(frame, bound, args, mode); + return call_callable_with_mode(execution_context, bound, args, mode); } // The base ClassMethod defines no descr_call (function.py), so a raw // classmethod object falls through to the not-callable error. @@ -1561,7 +1679,7 @@ fn call_non_function_callable_with_mode( let mut call_args = Vec::with_capacity(1 + current_args.len()); call_args.push(current_callable); call_args.extend_from_slice(¤t_args); - return call_callable_with_mode(frame, call_fn, &call_args, mode); + return call_callable_with_mode(execution_context, call_fn, &call_args, mode); } // `user_call_slot`'s stack_check bounds a self-referential // `A.__call__ = A()` chain only while this self-dispatch recurses @@ -1569,7 +1687,7 @@ fn call_non_function_callable_with_mode( // dropping only after the call returns, keeps it off the tail so LLVM // cannot rewrite the self-call into a loop that never grows the stack. let _depth_guard = enter_native_dispatch(); - return call_callable_with_mode(frame, call_fn, ¤t_args, mode); + return call_callable_with_mode(execution_context, call_fn, ¤t_args, mode); } // GenericAlias.__call__ (`_pypy_generic_alias.py:41`) — @@ -1577,12 +1695,12 @@ fn call_non_function_callable_with_mode( // `result.__orig_class__ = self`. if unsafe { pyre_object::is_generic_alias(callable) } { let origin = unsafe { pyre_object::w_generic_alias_get_origin(callable) }; - let result = call_callable_with_mode(frame, origin, args, mode)?; + let result = call_callable_with_mode(execution_context, origin, args, mode)?; set_orig_class(result, callable)?; return Ok(result); } - call_function_carrier_with_mode(frame, callable, args, mode) + call_function_carrier_with_mode(execution_context, callable, args, mode) } pub fn call_user_function( @@ -1673,7 +1791,8 @@ pub fn call_callable_inline_residual( callable: PyObjectRef, args: &[PyObjectRef], ) -> PyResult { - call_callable_with_mode(frame, callable, args, CallMode::Plain) + let _caller_locals_root = FrameLocalsRoot::new(frame); + call_callable_with_mode(frame.execution_context, callable, args, CallMode::Plain) } // ── __build_class__ implementation ─────────────────────────────────── @@ -2252,13 +2371,25 @@ pub(crate) fn bind_kwargs_to_signature( Ok(result) } +/// [`call_with_kwargs_in_ctx`] reached from a frame. Installs the caller +/// `FrameLocalsRoot` the way [`call_callable`] does. +pub fn call_with_kwargs( + frame: &mut crate::pyframe::PyFrame, + callable: PyObjectRef, + pos_args: &[PyObjectRef], + kwargs: &[(Wtf8Buf, PyObjectRef)], +) -> PyResult { + let _caller_locals_root = FrameLocalsRoot::new(frame); + call_with_kwargs_in_ctx(frame.execution_context, callable, pos_args, kwargs) +} + /// Call a user function with positional args + keyword args from a dict. /// /// PyPy: argument.py Arguments._match_signature with keyword handling. /// Used by CALL_FUNCTION_KW / CALL_KW and CALL_FUNCTION_EX when kwargs /// are non-empty. -pub fn call_with_kwargs( - frame: &mut crate::pyframe::PyFrame, +pub fn call_with_kwargs_in_ctx( + execution_context: *const crate::PyExecutionContext, callable: PyObjectRef, pos_args: &[PyObjectRef], kwargs: &[(Wtf8Buf, PyObjectRef)], @@ -2287,15 +2418,15 @@ pub fn call_with_kwargs( // collections unchanged to its w_function. if unsafe { pyre_object::is_exact_type(callable, &pyre_object::function::STATICMETHOD_TYPE) } { let func = unsafe { pyre_object::w_staticmethod_get_func(callable) }; - return call_with_kwargs(frame, func, pos_args, kwargs); + return call_with_kwargs_in_ctx(execution_context, func, pos_args, kwargs); } if let Some(bound) = staticmethod_call_override(callable)? { - return call_with_kwargs(frame, bound, pos_args, kwargs); + return call_with_kwargs_in_ctx(execution_context, bound, pos_args, kwargs); } if unsafe { pyre_object::is_classmethod(callable) } { if let Some(bound) = classmethod_call_override(callable)? { - return call_with_kwargs(frame, bound, pos_args, kwargs); + return call_with_kwargs_in_ctx(execution_context, bound, pos_args, kwargs); } let type_name = crate::typedef::r#type(callable) .map(|tp| unsafe { pyre_object::w_type_get_name(tp.as_ptr()) }) @@ -2314,14 +2445,14 @@ pub fn call_with_kwargs( full_args.push(receiver); } full_args.extend_from_slice(pos_args); - return call_with_kwargs(frame, func, &full_args, kwargs); + return call_with_kwargs_in_ctx(execution_context, func, &full_args, kwargs); } // A class call routes through `type(cls).__call__` when the metaclass // overrides it (enum functional API passes `module=`/`type=` kwargs). if unsafe { pyre_object::is_type(callable) } { if let Some(bound) = metaclass_call_override(callable) { - return call_with_kwargs(frame, bound, pos_args, kwargs); + return call_with_kwargs_in_ctx(execution_context, bound, pos_args, kwargs); } } @@ -2365,8 +2496,8 @@ pub fn call_with_kwargs( // `c_call_trace` / `c_return_trace`, so route the bound flat // slice through the profile-aware path like the marker branch // below rather than invoking the builtin directly. - let frame_ptr = frame as *mut PyFrame; - if unsafe { (*frame_ptr).get_is_being_profiled() } { + let frame_ptr = c_profile_frame(execution_context); + if !frame_ptr.is_null() { let keyword_names_w: Vec = kwargs .iter() .map(|(k, _)| pyre_object::w_str_from_wtf8(k.clone())) @@ -2467,9 +2598,8 @@ pub fn call_with_kwargs( // breaking the FunctionWithFixedCode rebinding's // firstarg() (`argument.py:164-168` returns `None` // when positional count is zero, not the kwargs dict). - let frame_ptr = frame as *mut PyFrame; - let profile_active = unsafe { (*frame_ptr).get_is_being_profiled() }; - if profile_active { + let frame_ptr = c_profile_frame(execution_context); + if !frame_ptr.is_null() { let keyword_names_w: Vec = kwargs .iter() .map(|(k, _)| pyre_object::w_str_from_wtf8(k.clone())) @@ -2505,7 +2635,7 @@ pub fn call_with_kwargs( return Ok(w_res); } } - return call_callable(frame, callable, &full_args); + return call_callable_in_ctx(execution_context, callable, &full_args); } // For user functions: resolve kwargs to parameter slots @@ -2759,7 +2889,7 @@ pub fn call_with_kwargs( w_code, &final_args, w_globals, - frame.execution_context, + execution_context, closure, crate::pyframe::FrameLocalsArrayAllocation::OldGenGc, )?, @@ -2859,9 +2989,9 @@ pub fn call_with_kwargs( new_args.push(current_metaclass()); new_args.extend_from_slice(pos_args); if unsafe { crate::is_function(new_fn) } && !kwargs.is_empty() { - call_with_kwargs(frame, new_fn, &new_args, kwargs)? + call_with_kwargs_in_ctx(execution_context, new_fn, &new_args, kwargs)? } else { - call_callable(frame, new_fn, &new_args)? + call_callable_in_ctx(execution_context, new_fn, &new_args)? } } else { pyre_object::w_instance_new(current_type()) @@ -2895,7 +3025,7 @@ pub fn call_with_kwargs( // descriptor dispatch. init_args.push(pyre_object::gc_roots::shadow_stack_get(instance_slot)); init_args.extend_from_slice(pos_args); - call_with_kwargs(frame, init_descr, &init_args, kwargs)? + call_with_kwargs_in_ctx(execution_context, init_descr, &init_args, kwargs)? } else { let init_fn = unsafe { crate::baseobjspace::get( @@ -2905,7 +3035,7 @@ pub fn call_with_kwargs( )? } .unwrap_or(init_descr); - call_with_kwargs(frame, init_fn, pos_args, kwargs)? + call_with_kwargs_in_ctx(execution_context, init_fn, pos_args, kwargs)? }; check_init_returned_none(init_result)?; } @@ -2921,7 +3051,7 @@ pub fn call_with_kwargs( full_args.push(w_self); } full_args.extend_from_slice(pos_args); - return call_with_kwargs(frame, func, &full_args, kwargs); + return call_with_kwargs_in_ctx(execution_context, func, &full_args, kwargs); } if let Some((call_fn, prepend_receiver)) = user_call_slot(current_callable())? { @@ -2938,13 +3068,23 @@ pub fn call_with_kwargs( let mut call_args = Vec::with_capacity(1 + current_pos_args.len()); call_args.push(current_callable()); call_args.extend_from_slice(¤t_pos_args); - return call_with_kwargs(frame, call_fn, &call_args, ¤t_kwargs); + return call_with_kwargs_in_ctx( + execution_context, + call_fn, + &call_args, + ¤t_kwargs, + ); } // Depth guard: count this dispatch level and, dropping after the call, // keep it off the tail so a self-referential `A.__call__ = A()` // recurses natively for stack_check (see call_callable_with_mode). let _depth_guard = enter_native_dispatch(); - return call_with_kwargs(frame, call_fn, ¤t_pos_args, ¤t_kwargs); + return call_with_kwargs_in_ctx( + execution_context, + call_fn, + ¤t_pos_args, + ¤t_kwargs, + ); } // GenericAlias.__call__ (`_pypy_generic_alias.py:41`) — @@ -2952,13 +3092,13 @@ pub fn call_with_kwargs( // `result.__orig_class__ = self`. if unsafe { pyre_object::is_generic_alias(callable) } { let origin = unsafe { pyre_object::w_generic_alias_get_origin(callable) }; - let result = call_with_kwargs(frame, origin, pos_args, kwargs)?; + let result = call_with_kwargs_in_ctx(execution_context, origin, pos_args, kwargs)?; set_orig_class(result, callable)?; return Ok(result); } // Fallback: call_callable with positional args only - call_callable(frame, callable, pos_args) + call_callable_in_ctx(execution_context, callable, pos_args) } pub fn register_build_class() { @@ -4763,7 +4903,7 @@ pub(crate) fn call_init_subclass_on_bases( // PyPy equivalent: typeobject.py descr_call → __new__ + __init__ fn type_descr_call_with_mode( - frame: &mut PyFrame, + execution_context: *const crate::PyExecutionContext, w_type: PyObjectRef, args: &[PyObjectRef], mode: CallMode, @@ -4793,7 +4933,7 @@ fn type_descr_call_with_mode( let mut new_args = Vec::with_capacity(1 + args.len()); new_args.push(w_type); new_args.extend_from_slice(args); - let instance = call_callable_with_mode(frame, new_fn, &new_args, mode)?; + let instance = call_callable_with_mode(execution_context, new_fn, &new_args, mode)?; let _instance_roots = pyre_object::gc_roots::push_roots(); let instance_slot = pyre_object::gc_roots::shadow_stack_len(); pyre_object::gc_roots::pin_root(instance); @@ -4811,13 +4951,13 @@ fn type_descr_call_with_mode( let mut init_args = Vec::with_capacity(1 + args.len()); init_args.push(current_instance()); init_args.extend_from_slice(args); - call_callable_with_mode(frame, init_descr, &init_args, mode)? + call_callable_with_mode(execution_context, init_descr, &init_args, mode)? } else { let init_fn = unsafe { crate::baseobjspace::get(init_descr, current_instance(), w_insttype)? } .unwrap_or(init_descr); - call_callable_with_mode(frame, init_fn, args, mode)? + call_callable_with_mode(execution_context, init_fn, args, mode)? }; check_init_returned_none(init_result)?; } diff --git a/pyre/pyre-jit/src/call_jit.rs b/pyre/pyre-jit/src/call_jit.rs index ccec01e78e6..3682080f629 100644 --- a/pyre/pyre-jit/src/call_jit.rs +++ b/pyre/pyre-jit/src/call_jit.rs @@ -4770,9 +4770,11 @@ bh_call_fn_arity!(bh_call_fn_13; a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a1 bh_call_fn_arity!(bh_call_fn_14; a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13); /// CALL_KW residual shared body (`call_kw(callable, self_or_null, -/// positional, kwnames)` HLOp → per-arity `residual_call_r_r`). Resolves -/// the parent frame from the execution context like [`bh_call_fn_impl`], -/// then runs keyword resolution + the dispatched call under +/// positional, kwnames)` HLOp → per-arity `residual_call_r_r`). Carries no +/// frame, like [`bh_call_fn_impl`]: `pyopcode.py:1402 CALL_FUNCTION_KW` +/// settles the C-profile question against its own frame and then calls the +/// frameless `space.call_args(w_function, args)`, which is what a residual +/// reaches. Keyword resolution + the dispatched call run under /// `force_plain_eval` (blackhole.py:1225 `bhimpl_residual_call_*` is an /// opaque CPU call — no JIT re-entry; `call_kw`'s /// `call_user_function_resolved` fast path routes through the JIT-aware @@ -4787,25 +4789,17 @@ fn bh_call_kw_impl( positional: &[PyObjectRef], ) -> i64 { let ec = pyre_interpreter::call::getexecutioncontext(); - let parent_frame_ptr: *const PyFrame = if ec.is_null() { - std::ptr::null() - } else { - unsafe { (*ec).gettopframe_raw() as *const PyFrame } - }; assert!( - !parent_frame_ptr.is_null(), - "bh_call_kw_impl requires a live parent PyFrame from \ - getexecutioncontext().gettopframe_raw(); the eval loop must pin the \ - execution context before any residual call" + !ec.is_null(), + "bh_call_kw_impl requires a pinned execution context from \ + getexecutioncontext(); the eval loop must pin the execution context \ + before any residual call" ); let saved_ctx = pyre_interpreter::call::take_last_exec_ctx(); - unsafe { - pyre_interpreter::call::set_last_exec_ctx((*parent_frame_ptr).execution_context); - } - let parent_frame = unsafe { &mut *(parent_frame_ptr as *mut PyFrame) }; + pyre_interpreter::call::set_last_exec_ctx(ec); let result = { let _plain_guard = pyre_interpreter::call::force_plain_eval(); - pyre_interpreter::call::call_kw(parent_frame, callable, null_or_self, positional, kwnames) + pyre_interpreter::call::call_kw_in_ctx(ec, callable, null_or_self, positional, kwnames) }; pyre_interpreter::call::set_last_exec_ctx(saved_ctx); match result { @@ -5020,8 +5014,7 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO // `GUARD_NO_EXCEPTION` as a spurious pending exception (the failure // `drain_backend_jit_exc` names for the walker's snapshot side). let parked = park_residual_call_exception(); - let result = - pyre_interpreter::call::call_user_function_residual_with_ctx(ec, callable, &call_args); + let result = pyre_interpreter::call::call_user_function_with_ctx(ec, callable, &call_args); unpark_residual_call_exception(parked); pyre_interpreter::call::set_last_exec_ctx(saved_ctx); return match result { @@ -5058,9 +5051,11 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO /// CALL_FUNCTION_EX residual (`call_function_ex(callable, self_or_null, /// starargs, kwargs_or_null)` HLOp → `residual_call_r_r`). Unpacks the /// `*` iterable and merges the `**` mapping through the shared -/// `call::call_function_ex`, then dispatches. Resolves the parent frame -/// from the execution context like [`bh_call_fn_impl`], and runs the -/// nested Python call under `force_plain_eval` (blackhole.py:1225 +/// `call::call_function_ex`, then dispatches. Carries no frame, like +/// [`bh_call_fn_impl`]: `pyopcode.py:1429 CALL_FUNCTION_EX` settles the +/// C-profile question against its own frame and then calls the frameless +/// `space.call_args(w_function, args)`, which is what a residual reaches. +/// The nested Python call runs under `force_plain_eval` (blackhole.py:1225 /// `bhimpl_residual_call_*` is an opaque CPU call — no JIT re-entry). /// MayForce: unpacking an arbitrary iterable / mapping and the dispatched /// call may run Python. @@ -5071,26 +5066,18 @@ pub extern "C" fn bh_call_function_ex_fn( kwargs_or_null: i64, ) -> i64 { let ec = pyre_interpreter::call::getexecutioncontext(); - let parent_frame_ptr: *const PyFrame = if ec.is_null() { - std::ptr::null() - } else { - unsafe { (*ec).gettopframe_raw() as *const PyFrame } - }; assert!( - !parent_frame_ptr.is_null(), - "bh_call_function_ex_fn requires a live parent PyFrame from \ - getexecutioncontext().gettopframe_raw(); the eval loop must pin the \ - execution context before any residual call" + !ec.is_null(), + "bh_call_function_ex_fn requires a pinned execution context from \ + getexecutioncontext(); the eval loop must pin the execution context \ + before any residual call" ); let saved_ctx = pyre_interpreter::call::take_last_exec_ctx(); - unsafe { - pyre_interpreter::call::set_last_exec_ctx((*parent_frame_ptr).execution_context); - } - let parent_frame = unsafe { &mut *(parent_frame_ptr as *mut PyFrame) }; + pyre_interpreter::call::set_last_exec_ctx(ec); let result = { let _plain_guard = pyre_interpreter::call::force_plain_eval(); - pyre_interpreter::call::call_function_ex( - parent_frame, + pyre_interpreter::call::call_function_ex_in_ctx( + ec, callable as PyObjectRef, self_or_null as PyObjectRef, starargs as PyObjectRef, From 6c84bc8bc8498bb2ff402df047037c0b06515f76 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 5 Aug 2026 19:58:42 +0900 Subject: [PATCH 4/6] jit: route kept-stack branch aborts through the CALL-forward flush MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `latch_abort_call_resume` and `run_perfn_walk`'s `call_forward_abort` each listed the same two DispatchError variants, so a walk that aborted with `BranchGuardUnrestorableKeptStackPermanent` or `BranchGuardKeptStackUnsupported` inside a top-level inline sub-walk had no gh#467 carrier. Its remaining leg, the kept-stack branch flush, declines on `abort_in_subwalk`, and the legacy drop behind that decline re-enters the outer frame at its entry — re-running the residual calls the authoritative walk had already executed concretely. `threading.Thread.start` then calls `_start_joinable_thread` a second time on a handle that is already started. Both allowlists now carry the two kept-stack variants. The soundness gate is unchanged and central: `try_commit_entry_carrier_call` commits `WalkEndResume::Rewind { effects_at_resume_point }`, which `commit_walk_end` declines when the executed-effect odometer moved since the outer CALL, and `latch_abort_call_resume` latches only for a top-level inline whose callee executed no concrete effect. A third field on `call_forward_abort` keeps the new variants off the `MidBody` carrier, which only the original two latch. The kept-stack flush leg now checks `WALK_END_FLUSH_COMMITTED` first so it cannot reposition a frame the carrier leg already flushed. `[fbw-abort]` additionally names the walked code object and the executed-effect count under `PYRE_FBW_DEBUG_ABORT`. Measured, dynasm and cranelift: the new parity test fails 4/4 on the parent commit (deterministically, `RuntimeError: thread already started`) and passes 4/4 with the change; `check.py --backend dynasm,cranelift` reports no functional failures and no jit-stats delta attributable to the change (the two `improved: guard_failures` rows reproduce identically on a parent-commit binary); `cargo test --workspace --features dynasm` and the parity suite are green. Assisted-by: Claude --- .../thread_start_walk_abort_no_replay.py | 116 ++++++++++++++++++ .../src/jitcode_dispatch/inline_call.rs | 10 ++ pyre/pyre-jit-trace/src/trace.rs | 61 +++++++-- 3 files changed, 177 insertions(+), 10 deletions(-) create mode 100644 pyre/extra_tests/parity_tests/thread_start_walk_abort_no_replay.py diff --git a/pyre/extra_tests/parity_tests/thread_start_walk_abort_no_replay.py b/pyre/extra_tests/parity_tests/thread_start_walk_abort_no_replay.py new file mode 100644 index 00000000000..0c9dd5ad4ae --- /dev/null +++ b/pyre/extra_tests/parity_tests/thread_start_walk_abort_no_replay.py @@ -0,0 +1,116 @@ +"""A walk that aborts after executing a side-effecting residual must not replay it. + +An authoritative full-body walk executes residual calls concretely as it goes. +When it then aborts on a kept-stack branch guard it cannot compile, the only +sound continuations resume FORWARD (`pyjitpl.py:2949 +run_blackhole_interp_to_cancel_tracing`); handing the frame back at its entry +re-runs everything the walk already applied. `threading.Thread.start` is where +that is observable rather than merely wasteful: the walk runs +`_start_joinable_thread`, which spawns the OS thread, and a replay calls it a +second time on a handle that is already started. + +The three faces of that single replay, depending on how far the freshly spawned +worker got before the interpreter re-entered `start`: + + - the worker had already run `_started.set()` -> the fresh `Event` this object + built in `__init__`, still unpublished, reads set, + - it had not -> `RuntimeError: thread already started`, + - with the stock `Thread.start`, whose own guard sees the set Event -> + `RuntimeError: threads can only be started once`. + +⚠️ This file is a REPRODUCER, not a tidy test, and the difference matters. The +walk only reaches the aborting guard for a narrow traced shape: `start` spelled +out instead of delegating, the three Event reads kept, `Event` bound as a global +rather than reached through `threading.`, the two locals read inside the final +`try`, and failures leaving through `die` (a plain call) rather than `raise`, +whose exception edge changes the guard. Every one of those was measured — each +simplification made the file pass against a build WITHOUT the fix, i.e. cover +nothing. Re-measure against such a build before simplifying anything here. + +Every acquire on the main thread is bounded so a lost release reports instead of +hanging, and `die` uses `os._exit` so a failure cannot block on shutdown. +""" + +import contextvars +import os +import threading +from threading import Event + +N = 20 +R = 150 + + +def die(msg): + os.write(2, msg.encode()) + os._exit(1) + + +class T(threading.Thread): + def __init__(self, *a, **kw): + threading.Thread.__init__(self, *a, **kw) + self._ev_ref = self._started + if self._started._flag: + die("BORN TRUE: fresh Event already set at __init__ id=%d\n" + % id(self._started)) + + def start(self): + pre = self._started + if pre is not self._ev_ref: + die("STALE LOAD_ATTR self._started: got id=%d %r, stored id=%d %r\n" + % (id(pre), pre, id(self._ev_ref), self._ev_ref)) + if not isinstance(pre, Event): + die("WRONG SLOT (pre) %r %s\n" % (pre, type(pre))) + f_call = pre.is_set() + f_attr = pre._flag + f_call2 = pre.is_set() + if not (f_call is f_attr is f_call2): + die("DISAGREE call=%r attr=%r call2=%r ev_id=%d\n" + % (f_call, f_attr, f_call2, id(pre))) + if f_call: + die("FLIPPED AFTER INIT: a fresh Event reads set before start() " + "published it — start() ran twice. ev_id=%d thread_id=%d\n" + % (id(pre), id(self))) + if not threading._active_limbo_lock.acquire(True, 5.0): + die("BLOCKED on _active_limbo_lock\n") + try: + threading._limbo[self] = self + finally: + threading._active_limbo_lock.release() + if self._context is None: + self._context = contextvars.Context() + threading._start_joinable_thread( + self._bootstrap, handle=self._os_thread_handle, daemon=self.daemon) + post = self._started + if not isinstance(post, Event): + die("WRONG SLOT (post) %r %s same=%s\n" % (post, type(post), post is pre)) + # Event.wait(), with the outer Condition acquire bounded. + cond = post._cond + if not cond._lock.acquire(True, 5.0): + die("LOST RELEASE on _started._cond._lock: flag=%r alive=%r waiters=%d\n" + % (post._flag, self.is_alive(), len(cond._waiters))) + try: + alive_before = self.is_alive() + w_before = len(cond._waiters) + if not post._flag and not cond.wait(5.0): + die("NO SIGNAL | check: alive=%r waiters=%d | now: flag=%r " + "alive=%r waiters=%d\n" + % (alive_before, w_before, post._flag, self.is_alive(), + len(cond._waiters))) + finally: + cond._lock.release() + + +for r in range(R): + ts = [T(target=lambda: None) for _ in range(N)] + try: + for t in ts: + t.start() + except RuntimeError as e: + die("START RAISED %r in round %d — start() ran twice on one handle\n" + % (e, r)) + for i, t in enumerate(ts): + t.join(5.0) + if t.is_alive(): + die("JOIN TIMEOUT round=%d idx=%d\n" % (r, i)) + +print("OK") 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 63d012fceb0..904c3fbef34 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs @@ -4365,10 +4365,20 @@ pub(crate) fn try_walker_inline_resolved_user_call( // not the pre-sub-walk `arg_concretes`, so it is current after the // sub-walk's allocations. Any doubt keeps the legacy replay — the // honest residual (the inner-frame rebuild is #126/#215). + // + // The kept-stack branch-guard aborts belong to the same class: they + // refuse to COMPILE a guard whose not-taken arm the blackhole could + // not reconstruct, which says nothing about the sub-walk having + // committed anything. Without a carrier their only remaining leg + // rewinds to the OUTER frame's entry (`trace.rs`, "legacy drop + // kept"), re-running every effect the walk already executed — + // `threading.Thread.start` calling `_start_joinable_thread` twice. if matches!( e, DispatchError::AbortPermanentMarkerReached { .. } | DispatchError::LoopBearingCalleeInlineUnsupported { .. } + | DispatchError::BranchGuardUnrestorableKeptStackPermanent { .. } + | DispatchError::BranchGuardKeptStackUnsupported { .. } ) { latch_abort_call_resume( code, diff --git a/pyre/pyre-jit-trace/src/trace.rs b/pyre/pyre-jit-trace/src/trace.rs index 2390eb5e9a0..08c60635ee6 100644 --- a/pyre/pyre-jit-trace/src/trace.rs +++ b/pyre/pyre-jit-trace/src/trace.rs @@ -4087,17 +4087,29 @@ fn run_perfn_walk( { crate::jitcode_dispatch::restore_escape_flush_undo(); } + // The third field marks an abort that may only consume the `Entry` + // carrier: the `MidBody` carrier is latched exclusively by the first two + // variants (`inline_call.rs`), so a kept-stack abort matching a MidBody + // payload would be reading another abort's rebuild plan. let call_forward_abort = match &walk_result { Err(crate::jitcode_dispatch::DispatchError::AbortPermanentMarkerReached { pc }) => { - Some((*pc, true)) + Some((*pc, true, false)) } Err(crate::jitcode_dispatch::DispatchError::LoopBearingCalleeInlineUnsupported { pc, - }) => Some((*pc, false)), + }) => Some((*pc, false, false)), + Err( + crate::jitcode_dispatch::DispatchError::BranchGuardUnrestorableKeptStackPermanent { + pc, + }, + ) + | Err(crate::jitcode_dispatch::DispatchError::BranchGuardKeptStackUnsupported { pc }) => { + Some((*pc, false, true)) + } _ => None, }; let mut committed_entry_carrier_call_py_pc = None; - if let Some((abort_jit_pc, is_marker_abort)) = call_forward_abort { + if let Some((abort_jit_pc, is_marker_abort, entry_carrier_only)) = call_forward_abort { // gh#467: a supported abort fired inside a TOP-level inline // sub-walk whose callee executed no concrete effect // (`try_walker_inline_user_call` latched the carrier only under @@ -4131,12 +4143,13 @@ fn run_perfn_walk( ); } Some(crate::jitcode_dispatch::InlineAbortCarrier::MidBody(payload)) - if (is_marker_abort - && payload.abort_kind - == crate::jitcode_dispatch::MidBodyAbortKind::Marker) - || (!is_marker_abort + if !entry_carrier_only + && ((is_marker_abort && payload.abort_kind - == crate::jitcode_dispatch::MidBodyAbortKind::Structural) => + == crate::jitcode_dispatch::MidBodyAbortKind::Marker) + || (!is_marker_abort + && payload.abort_kind + == crate::jitcode_dispatch::MidBodyAbortKind::Structural)) => { let rebuilt = match resolve_midbody_flush_words(payload) { Some(words) => { @@ -4485,7 +4498,18 @@ fn run_perfn_walk( }; if let Some((pc, is_unsupported)) = kept_stack_abort_pc { let abort_jit_pc = pc; - if crate::jitcode_dispatch::fbw_has_unjournaled_effect() + if WALK_END_FLUSH_COMMITTED.with(|c| c.get()) { + // A walk commits at most ONE leg. The gh#467 CALL-forward block + // above now also serves these aborts, and its flush already + // repositioned the frame; flushing again here would move it a + // second time. + if crate::jitcode_dispatch::fbw_debug_abort_enabled() { + eprintln!( + "[fbw-branch-flush] declined at abort_jit_pc={abort_jit_pc} \ + (a carrier leg already committed this walk)" + ); + } + } else if crate::jitcode_dispatch::fbw_has_unjournaled_effect() || session.borrow().abort_in_subwalk { if crate::jitcode_dispatch::fbw_debug_abort_enabled() { @@ -5596,7 +5620,24 @@ fn full_body_walk_trace( use crate::jitcode_dispatch::DispatchError as DE; crate::jitcode_dispatch::census_record(e.variant_name()); if crate::jitcode_dispatch::fbw_debug_abort_enabled() { - eprintln!("[fbw-abort] start_pc={start_pc} Err={e:?}"); + let raw_code = + unsafe { pyre_interpreter::w_code_get_ptr(w_code as pyre_object::PyObjectRef) } + as *const CodeObject; + let (name, path) = if raw_code.is_null() { + ("", "") + } else { + unsafe { + ( + (*raw_code).obj_name.as_str(), + (*raw_code).source_path.as_str(), + ) + } + }; + eprintln!( + "[fbw-abort] start_pc={start_pc} Err={e:?} \ + code={name} src={path} effects={}", + crate::jitcode_dispatch::fbw_executed_effect_count(), + ); } match e { // A kept-stack branch guard whose not-taken arm reads an From d49bef22a13e5a0c0de22bfc18528cfbd58a515c Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 5 Aug 2026 22:22:11 +0900 Subject: [PATCH 5/6] bench: record the three guard_failures rows the CALL residual change lowers `exception_subclass_attrs` dynasm and cranelift 3 -> 2, and `inline_gate_operand_provenance` dynasm 5 -> 4. All three OS jobs on PR #1046 reported the identical value (macos-latest, ubuntu-24.04, windows-latest), so one number satisfies every host. `inline_gate_operand_provenance`'s optimized-trace op counts are unchanged across the change (61/61/61/49 before and after), so the move is deopt accounting rather than codegen. The two wasm rows check.py reports as regressed are not recorded. Assisted-by: Claude --- pyre/bench/synth/exception_subclass_attrs.cranelift.jitstats | 2 +- pyre/bench/synth/exception_subclass_attrs.dynasm.jitstats | 2 +- pyre/bench/synth/inline_gate_operand_provenance.dynasm.jitstats | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyre/bench/synth/exception_subclass_attrs.cranelift.jitstats b/pyre/bench/synth/exception_subclass_attrs.cranelift.jitstats index 9cf2e63b4a5..9d7d0e11958 100644 --- a/pyre/bench/synth/exception_subclass_attrs.cranelift.jitstats +++ b/pyre/bench/synth/exception_subclass_attrs.cranelift.jitstats @@ -2,7 +2,7 @@ bridges_compiled=0 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 -guard_failures=3 +guard_failures=2 internal_compile_panics=0 loops_aborted=0 loops_compiled=1 diff --git a/pyre/bench/synth/exception_subclass_attrs.dynasm.jitstats b/pyre/bench/synth/exception_subclass_attrs.dynasm.jitstats index 9cf2e63b4a5..9d7d0e11958 100644 --- a/pyre/bench/synth/exception_subclass_attrs.dynasm.jitstats +++ b/pyre/bench/synth/exception_subclass_attrs.dynasm.jitstats @@ -2,7 +2,7 @@ bridges_compiled=0 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 -guard_failures=3 +guard_failures=2 internal_compile_panics=0 loops_aborted=0 loops_compiled=1 diff --git a/pyre/bench/synth/inline_gate_operand_provenance.dynasm.jitstats b/pyre/bench/synth/inline_gate_operand_provenance.dynasm.jitstats index 1b1f3a7dd87..b36bfa6ee29 100644 --- a/pyre/bench/synth/inline_gate_operand_provenance.dynasm.jitstats +++ b/pyre/bench/synth/inline_gate_operand_provenance.dynasm.jitstats @@ -2,7 +2,7 @@ bridges_compiled=0 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 -guard_failures=5 +guard_failures=4 internal_compile_panics=0 loops_aborted=3 loops_compiled=7 From 4377be8e3c508c92a146706451967e16acf1659f Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 5 Aug 2026 23:15:40 +0900 Subject: [PATCH 6/6] bench: record the two wasm rows the CALL residual change moves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `foriter_call_resume_drops_iteration` loops_aborted 4 -> 2, guard_failures 5182 -> 5165, bridges_compiled 23 -> 26: two loops that aborted now compile, and all three fields move toward the dynasm and cranelift baselines (1 / 5150 / 27). `kept_stack_deep_var_shortcircuit` guard_failures 823 -> 824, with loops_compiled, bridges_compiled and loops_aborted unchanged. dynasm is bit-identical across the change (6 / 4 / 0 / 819 both before and after), so the count is wasm-side. The fixture has no self-recursive call, so `bh_call_self_recursive_portal` declines both before and after; what the change removes ahead of that decline is the `gettopframe_raw()` that forced and materialized the caller frame at every `g(i)` / `h(i)` residual. Both values are what ubuntu-24.04 measured on PR #1046 — the only host that runs the wasm backend — and reproduce three runs out of three locally. Restoring call.rs and call_jit.rs to their origin/main contents returns both rows to their previously recorded baselines. `fbw_rolled_back_with_effects` enters both files because 64880ab08fb added the counter. Assisted-by: Claude --- .../foriter_call_resume_drops_iteration.wasm.jitstats | 7 ++++--- .../synth/kept_stack_deep_var_shortcircuit.wasm.jitstats | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/pyre/bench/synth/foriter_call_resume_drops_iteration.wasm.jitstats b/pyre/bench/synth/foriter_call_resume_drops_iteration.wasm.jitstats index be492bf216b..46e2d700623 100644 --- a/pyre/bench/synth/foriter_call_resume_drops_iteration.wasm.jitstats +++ b/pyre/bench/synth/foriter_call_resume_drops_iteration.wasm.jitstats @@ -1,8 +1,9 @@ -bridges_compiled=23 +bridges_compiled=26 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 -guard_failures=5182 +fbw_rolled_back_with_effects=0 +guard_failures=5165 internal_compile_panics=0 -loops_aborted=4 +loops_aborted=2 loops_compiled=3 diff --git a/pyre/bench/synth/kept_stack_deep_var_shortcircuit.wasm.jitstats b/pyre/bench/synth/kept_stack_deep_var_shortcircuit.wasm.jitstats index c69a717cc1b..2321d60dcb6 100644 --- a/pyre/bench/synth/kept_stack_deep_var_shortcircuit.wasm.jitstats +++ b/pyre/bench/synth/kept_stack_deep_var_shortcircuit.wasm.jitstats @@ -2,7 +2,8 @@ bridges_compiled=4 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 -guard_failures=823 +fbw_rolled_back_with_effects=0 +guard_failures=824 internal_compile_panics=0 loops_aborted=0 loops_compiled=6