diff --git a/pyre/bench/synth/bridge_global_fold_invalidate_hot.cranelift.jitstats b/pyre/bench/synth/bridge_global_fold_invalidate_hot.cranelift.jitstats index 9d61fc87c05..80e7f9e6142 100644 --- a/pyre/bench/synth/bridge_global_fold_invalidate_hot.cranelift.jitstats +++ b/pyre/bench/synth/bridge_global_fold_invalidate_hot.cranelift.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=4 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=814 +guard_failures=1150 internal_compile_panics=0 loops_aborted=0 -loops_compiled=7 +loops_compiled=9 diff --git a/pyre/bench/synth/bridge_global_fold_invalidate_hot.dynasm.jitstats b/pyre/bench/synth/bridge_global_fold_invalidate_hot.dynasm.jitstats index 9d61fc87c05..80e7f9e6142 100644 --- a/pyre/bench/synth/bridge_global_fold_invalidate_hot.dynasm.jitstats +++ b/pyre/bench/synth/bridge_global_fold_invalidate_hot.dynasm.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=4 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=814 +guard_failures=1150 internal_compile_panics=0 loops_aborted=0 -loops_compiled=7 +loops_compiled=9 diff --git a/pyre/bench/synth/bridge_global_fold_invalidate_hot.wasm.jitstats b/pyre/bench/synth/bridge_global_fold_invalidate_hot.wasm.jitstats index 9d61fc87c05..80e7f9e6142 100644 --- a/pyre/bench/synth/bridge_global_fold_invalidate_hot.wasm.jitstats +++ b/pyre/bench/synth/bridge_global_fold_invalidate_hot.wasm.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=4 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=814 +guard_failures=1150 internal_compile_panics=0 loops_aborted=0 -loops_compiled=7 +loops_compiled=9 diff --git a/pyre/bench/synth/set_contains_frozenset.cranelift.jitstats b/pyre/bench/synth/set_contains_frozenset.cranelift.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_contains_frozenset.cranelift.jitstats +++ b/pyre/bench/synth/set_contains_frozenset.cranelift.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/bench/synth/set_contains_frozenset.dynasm.jitstats b/pyre/bench/synth/set_contains_frozenset.dynasm.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_contains_frozenset.dynasm.jitstats +++ b/pyre/bench/synth/set_contains_frozenset.dynasm.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/bench/synth/set_contains_frozenset.py b/pyre/bench/synth/set_contains_frozenset.py index 2f0f9e2d4be..ced6e7a8ee2 100644 --- a/pyre/bench/synth/set_contains_frozenset.py +++ b/pyre/bench/synth/set_contains_frozenset.py @@ -1,7 +1,7 @@ -# No `max-pypy-ratio`: this fixture compiles no loop -- its jitstats record -# `loops_compiled=0` -- so a pypy ratio compares two interpreters' startup -# rather than any generated code, and reads whatever the host's process -# spawn cost happens to be that run. The jitstats baselines gate it. +# No `max-pypy-ratio`: the only loop this fixture compiles is its warmup, so a +# pypy ratio reads mostly the two interpreters' startup rather than the set +# operations the fixture names, and picks up whatever the host's process spawn +# cost happens to be that run. The jitstats baselines gate it. # A set is unhashable, but when one is used to look an element up it stands # in for the frozenset holding the same elements, so `in`, discard and remove # find it. The element is hashed on the way, so a raising __hash__ propagates diff --git a/pyre/bench/synth/set_contains_frozenset.wasm.jitstats b/pyre/bench/synth/set_contains_frozenset.wasm.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_contains_frozenset.wasm.jitstats +++ b/pyre/bench/synth/set_contains_frozenset.wasm.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/bench/synth/set_intersection_operand.cranelift.jitstats b/pyre/bench/synth/set_intersection_operand.cranelift.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_intersection_operand.cranelift.jitstats +++ b/pyre/bench/synth/set_intersection_operand.cranelift.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/bench/synth/set_intersection_operand.dynasm.jitstats b/pyre/bench/synth/set_intersection_operand.dynasm.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_intersection_operand.dynasm.jitstats +++ b/pyre/bench/synth/set_intersection_operand.dynasm.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/bench/synth/set_intersection_operand.py b/pyre/bench/synth/set_intersection_operand.py index 00e38749b2e..9d161acee9f 100644 --- a/pyre/bench/synth/set_intersection_operand.py +++ b/pyre/bench/synth/set_intersection_operand.py @@ -1,7 +1,7 @@ -# No `max-pypy-ratio`: this fixture compiles no loop -- its jitstats record -# `loops_compiled=0` -- so a pypy ratio compares two interpreters' startup -# rather than any generated code, and reads whatever the host's process -# spawn cost happens to be that run. The jitstats baselines gate it. +# No `max-pypy-ratio`: the only loop this fixture compiles is its warmup, so a +# pypy ratio reads mostly the two interpreters' startup rather than the set +# operations the fixture names, and picks up whatever the host's process spawn +# cost happens to be that run. The jitstats baselines gate it. # An intersection walks the shorter side and keeps that side's objects, so # when two equal elements are distinct objects which one survives depends on # the operand lengths. The shortest operand seeds the result, measured as diff --git a/pyre/bench/synth/set_intersection_operand.wasm.jitstats b/pyre/bench/synth/set_intersection_operand.wasm.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_intersection_operand.wasm.jitstats +++ b/pyre/bench/synth/set_intersection_operand.wasm.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/bench/synth/set_update_hash_other.cranelift.jitstats b/pyre/bench/synth/set_update_hash_other.cranelift.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_update_hash_other.cranelift.jitstats +++ b/pyre/bench/synth/set_update_hash_other.cranelift.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/bench/synth/set_update_hash_other.dynasm.jitstats b/pyre/bench/synth/set_update_hash_other.dynasm.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_update_hash_other.dynasm.jitstats +++ b/pyre/bench/synth/set_update_hash_other.dynasm.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/bench/synth/set_update_hash_other.py b/pyre/bench/synth/set_update_hash_other.py index 5d91da1ac5b..87c19f27414 100644 --- a/pyre/bench/synth/set_update_hash_other.py +++ b/pyre/bench/synth/set_update_hash_other.py @@ -1,7 +1,7 @@ -# No `max-pypy-ratio`: this fixture compiles no loop -- its jitstats record -# `loops_compiled=0` -- so a pypy ratio compares two interpreters' startup -# rather than any generated code, and reads whatever the host's process -# spawn cost happens to be that run. The jitstats baselines gate it. +# No `max-pypy-ratio`: the only loop this fixture compiles is its warmup, so a +# pypy ratio reads mostly the two interpreters' startup rather than the set +# operations the fixture names, and picks up whatever the host's process spawn +# cost happens to be that run. The jitstats baselines gate it. # difference_update and intersection_update hash each element of the other # operand as it is consumed, so an unhashable element raises and a raising # __hash__ propagates -- including when self is empty and nothing can match. diff --git a/pyre/bench/synth/set_update_hash_other.wasm.jitstats b/pyre/bench/synth/set_update_hash_other.wasm.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_update_hash_other.wasm.jitstats +++ b/pyre/bench/synth/set_update_hash_other.wasm.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/bench/synth/set_update_materialize_rhs.cranelift.jitstats b/pyre/bench/synth/set_update_materialize_rhs.cranelift.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_update_materialize_rhs.cranelift.jitstats +++ b/pyre/bench/synth/set_update_materialize_rhs.cranelift.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/bench/synth/set_update_materialize_rhs.dynasm.jitstats b/pyre/bench/synth/set_update_materialize_rhs.dynasm.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_update_materialize_rhs.dynasm.jitstats +++ b/pyre/bench/synth/set_update_materialize_rhs.dynasm.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/bench/synth/set_update_materialize_rhs.py b/pyre/bench/synth/set_update_materialize_rhs.py index b67f524b115..6f7eed1280b 100644 --- a/pyre/bench/synth/set_update_materialize_rhs.py +++ b/pyre/bench/synth/set_update_materialize_rhs.py @@ -1,7 +1,7 @@ -# No `max-pypy-ratio`: this fixture compiles no loop -- its jitstats record -# `loops_compiled=0` -- so a pypy ratio compares two interpreters' startup -# rather than any generated code, and reads whatever the host's process -# spawn cost happens to be that run. The jitstats baselines gate it. +# No `max-pypy-ratio`: the only loop this fixture compiles is its warmup, so a +# pypy ratio reads mostly the two interpreters' startup rather than the set +# operations the fixture names, and picks up whatever the host's process spawn +# cost happens to be that run. The jitstats baselines gate it. # symmetric_difference_update turns a non-set operand into a set before it # toggles anything, so the operand is hashed and deduped up front: a duplicate # toggles once, and a later unhashable element leaves self untouched. update diff --git a/pyre/bench/synth/set_update_materialize_rhs.wasm.jitstats b/pyre/bench/synth/set_update_materialize_rhs.wasm.jitstats index 1434ae6944f..59f22855e15 100644 --- a/pyre/bench/synth/set_update_materialize_rhs.wasm.jitstats +++ b/pyre/bench/synth/set_update_materialize_rhs.wasm.jitstats @@ -8,7 +8,7 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=0 +loops_compiled=1 diff --git a/pyre/check.py b/pyre/check.py index e49edf5b61f..3e19d38aead 100644 --- a/pyre/check.py +++ b/pyre/check.py @@ -1441,11 +1441,20 @@ def _header_directive(path, prefix): def _positive_float(raw, line, path, kind): - """A directive value that has to parse as a number above zero.""" + """A directive value that has to parse as a finite number above zero. + + `float()` accepts `inf` and `nan`, and neither is a threshold: a reading + compares below `inf` forever, and every comparison against `nan` is False, + so either one disarms the gate it configures while still looking like a + number in the header. `<= 0` does not catch them — `nan <= 0` is False — + so the finiteness test is separate. + """ try: value = float(raw) except ValueError as e: raise ValueError(f"invalid {kind} in {path}: {line.strip()}") from e + if not math.isfinite(value): + raise ValueError(f"{kind} must be finite in {path}: {line.strip()}") if value <= 0: raise ValueError(f"{kind} must be positive in {path}") return value diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs index 1f30041ef23..1c2af36fac0 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/bridge_subwalk.rs @@ -911,6 +911,8 @@ pub(crate) fn recipe_parent_frame_from_recipe( ctx: &mut TraceCtx, recipe: &majit_metainterp::ReconstructRecipe, root_ec: *const pyre_interpreter::PyExecutionContext, + root_ec_box: majit_ir::OpRef, + root_frame_box: majit_ir::OpRef, ) -> Option { let pjc = crate::state::pyjitcode_for_jitcode_index(recipe.jitcode_index)?; if !pjc.is_populated() || pjc.code_ptr.is_null() { @@ -936,8 +938,14 @@ pub(crate) fn recipe_parent_frame_from_recipe( // `pending.sym.frame` / `pending.sym.execution_context` are consumed here; // the `argboxes_r` register seeding is for the forward drive, not the // snapshot. - let (pending, _argboxes_r) = - crate::state::setup_reconstructed_callee_frame(ctx, recipe, root_ec, Vec::new())?; + let (pending, _argboxes_r) = crate::state::setup_reconstructed_callee_frame( + ctx, + recipe, + root_ec, + root_ec_box, + root_frame_box, + Vec::new(), + )?; let frame_box = pending.sym.frame(); let ec_box = pending.sym.execution_context(); @@ -1280,6 +1288,8 @@ pub(crate) fn drive_bridge_frame_subwalk( ctx, parent_recipe, root_sym.concrete_execution_context(), + root_sym.execution_context(), + root_sym.frame(), )?; } 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 60da6a9a738..a223d0b0d91 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs @@ -3128,6 +3128,83 @@ fn resolved_inline_decline(op_pc: usize, line: u32) -> Result, Disp Ok(None) } +/// The byte ranges of `code`'s exception handlers. A handler begins at a table +/// entry's `target` and covers the contiguous run of table ranges that starts +/// there, up to where the next handler begins. +/// +/// One `except` body routinely spans several table ranges: binding the caught +/// exception to a name, or a handler body that can itself raise, emits the +/// cleanup as further ranges whose `start` is nobody's `target`. Accepting only +/// the range that a `target` opens therefore sees the first few opcodes of the +/// handler and nothing after them -- for `except E as m: ... raise` it stops +/// before the `raise`. +fn callee_handler_spans(code: &pyre_interpreter::CodeObject) -> Vec<(u32, u32)> { + let mut entries: Vec<_> = + pyre_interpreter::pycode::decode_exceptiontable(&code.exceptiontable).collect(); + entries.sort_by_key(|entry| entry.start); + let targets: std::collections::HashSet = + entries.iter().map(|entry| entry.target).collect(); + let mut spans = Vec::new(); + for (index, entry) in entries.iter().enumerate() { + if !targets.contains(&entry.start) { + continue; + } + let mut end = entry.end; + for next in &entries[index + 1..] { + if next.start != end || targets.contains(&next.start) { + break; + } + end = next.end; + } + spans.push((entry.start, end)); + } + spans +} + +/// True when every `RAISE_VARARGS` in `code` is a bare re-raise sitting inside +/// one of the callee's own handlers -- the callee spells `except E: raise` and +/// holds no other way to raise. +/// +/// The screen exemption below rests on the abort coordinate being a handler's +/// own re-raise, so it has to hold for whichever raise the walk reaches, not +/// merely for one of them. Two shapes make the difference. A callee that also +/// raises on an ordinary path (`if bad: raise ValueError(x)`) can abort at that +/// raise, where the callee-rebuild would replay an effectful opcode the screen +/// exists to protect. And 3.14 emits a `finally` body twice, so a bare `raise` +/// there appears once inside the exceptional copy and once outside it, in the +/// normal copy -- where no exception is active and it raises `RuntimeError` +/// rather than re-raising. Requiring every raise to qualify declines both. +fn callee_only_bare_reraises_in_handlers(code: &pyre_interpreter::CodeObject) -> bool { + let spans = callee_handler_spans(code); + if spans.is_empty() { + return false; + } + let mut arg_state = pyre_interpreter::bytecode::OpArgState::default(); + let mut saw_raise = false; + for (pc, unit) in code.instructions.iter().copied().enumerate() { + let (instruction, op_arg) = arg_state.get(unit); + let pyre_interpreter::bytecode::Instruction::RaiseVarargs { argc } = instruction else { + continue; + }; + saw_raise = true; + if !matches!( + argc.get(op_arg), + pyre_interpreter::bytecode::oparg::RaiseKind::BareRaise + ) { + return false; + } + // The table is keyed by byte offset; `pc` is the instruction-unit index. + let byte_offset = (pc * 2) as u32; + if !spans + .iter() + .any(|(start, end)| (*start..*end).contains(&byte_offset)) + { + return false; + } + } + saw_raise +} + #[allow(clippy::too_many_arguments)] fn try_walker_inline_resolved_user_call_inner( ctx: &mut WalkContext<'_, '_, Sym>, @@ -3586,6 +3663,7 @@ fn try_walker_inline_resolved_user_call_inner( // does not inherit that route's DeferredCall admission. let mut foriter_deferred_admit = false; let mut foriter_dirty_bound = false; + let mut foriter_dirty_raise_handler_admit = false; if fbw_foriter_inflight_active() && !instance_next_seeded_route { let safety = fbw_callee_body_replay_safety( body.code, @@ -3680,11 +3758,29 @@ fn try_walker_inline_resolved_user_call_inner( foriter_deferred_admit } CalleeReplaySafety::Dirty => { - // A stored bound method has an explicit receiver and can use - // the multi-frame red-frame path below. Keep loop-bearing and - // recursive callees residual: either requires another loop - // header rather than one bounded callee walk. - foriter_dirty_bound = bound_method.is_some() + // `MetaInterp.perform_call` pushes one MIFrame for every + // inlined call; whether Python spelled it as `f(x)` or + // `obj.m(x)` does not change the callee frame or its exception + // edges. Route a supported CALL through the same multi-frame + // red-frame path below whichever way it is spelled -- the + // admission tests the callee body, not the call syntax. + // + // The generated resume chain is currently sound for one + // paused caller around a callee with a bare re-raise in its + // own handler when the callable is a trace constant. Keep + // red-polymorphic calls and the remaining Dirty shapes + // residual until their bridge snapshots can encode every + // callable-specific traceback node. A stored bound method + // reaches the path on `bound_method` alone as it always has; + // one that also meets the body terms takes the same screen + // exemption below, because it holds the same seeded frame. + let bare_reraise_handler = body_facts.has_exception_table + && callable_guard_op.is_constant() + && ctx.session.borrow().framestack.len() < 2 + && callee_only_bare_reraises_in_handlers(callee_code); + foriter_dirty_raise_handler_admit = entry_is_call_boundary && bare_reraise_handler; + foriter_dirty_bound = entry_is_call_boundary + && (bound_method.is_some() || bare_reraise_handler) && !pyre_interpreter::code_has_for_iter(callee_code) && !pyre_interpreter::code_is_self_recursive(callee_code); foriter_dirty_bound @@ -3699,12 +3795,8 @@ fn try_walker_inline_resolved_user_call_inner( arg_facts.iter().filter(|arg| arg.numeric).count(), ); } - // An unbound `Dirty` body is not admitted by seeding its frame. Its - // residual can raise, and the local `except` that catches it is a - // callee-owned catch edge the inline path does not compile, so the - // exception escapes the caller instead of being handled where the - // source handles it. Stored bound methods instead take the explicit - // multi-frame red-frame path above. + // A Dirty body is admitted only when the CALL boundary can seed its + // own MIFrame. Non-call specializer entries remain residual. if !legacy_admit { return resolved_inline_decline(op.pc, line!()); } @@ -3831,16 +3923,13 @@ fn try_walker_inline_resolved_user_call_inner( } else { None }; - if matches!(branchy_handler_safety, Some(s) if s != CalleeReplaySafety::Clean) { - // A branchy callee with its own exception handler can take a structural - // abort after an earlier effectful Python opcode. The current - // callee-rebuild payload resumes at the Python opcode owning the abort - // jitcode pc; it cannot yet carry a post-op stack anchor. Re-entering - // that opcode would repeat its residual effect (PyPy instead resumes - // the live MIFrame at its precise resumepc). Keep that callee on the - // ordinary residual path until the generated frame snapshot can - // represent the precise post-effect coordinate. A terminal raising - // callee without a handler retains its after-residual live anchor. + if matches!(branchy_handler_safety, Some(s) if s != CalleeReplaySafety::Clean) + && !foriter_dirty_raise_handler_admit + { + // Keep the legacy whole-body replay screen everywhere except the + // re-raise shape admitted above. That shape owns a seeded callee frame + // and follows RPython `MetaInterp.perform_call`: the live path is + // traced in its MIFrame and guards carry its precise resume coordinate. crate::jitcode_dispatch::census_record( if branchy_handler_safety == Some(CalleeReplaySafety::DeferredCall) { "InlineCallee::BranchyHandlerDeferredCall" @@ -8379,3 +8468,50 @@ pub(crate) fn dispatch_inline_call_dirf_kind( } } } + +#[cfg(test)] +mod tests { + use super::callee_only_bare_reraises_in_handlers; + + fn callee(source: &str) -> pyre_interpreter::CodeObject { + use pyre_interpreter::ConstantData; + let module = pyre_interpreter::compile_exec(source).expect("test code should compile"); + module + .constants + .iter() + .find_map(|constant| match constant { + ConstantData::Code { code } if code.obj_name.as_str() == "f" => { + Some((**code).clone()) + } + _ => None, + }) + .expect("module should define `f`") + } + + #[test] + fn only_a_handler_owned_bare_reraise_qualifies() { + // A handler that binds the caught exception spreads over several table + // ranges, and the `raise` lands past the first one -- accepting only the + // range a `target` opens rejects the very shape this admits. + for source in [ + "def f(x, g):\n try:\n return g(x)\n except ValueError:\n raise\n", + "def f(x, g):\n try:\n return g(x)\n except ValueError as m:\n raise\n", + ] { + assert!(callee_only_bare_reraises_in_handlers(&callee(source))); + } + for source in [ + // Duplicated `finally` body: the normal copy's bare raise sits + // outside every handler, where it raises `RuntimeError`. + "def f(x, g):\n try:\n return g(x)\n finally:\n raise\n", + // Reachable on an ordinary path, so the walk can abort at a raise + // the exemption does not account for. + "def f(x, g):\n try:\n if x:\n raise ValueError(x)\n return g(x)\n except ValueError:\n raise\n", + // Raises a second exception rather than re-raising. + "def f(x, g):\n try:\n return g(x)\n except ValueError:\n raise KeyError\n", + // No exception table at all. + "def f(x):\n raise ValueError(x)\n", + ] { + assert!(!callee_only_bare_reraises_in_handlers(&callee(source))); + } + } +} diff --git a/pyre/pyre-jit-trace/src/state.rs b/pyre/pyre-jit-trace/src/state.rs index a1dfa74ef9b..75f5831bac5 100644 --- a/pyre/pyre-jit-trace/src/state.rs +++ b/pyre/pyre-jit-trace/src/state.rs @@ -10922,6 +10922,14 @@ impl JitState for PyreJitState { .filter(|op| !op.is_none()) .unwrap_or(OpRef::NONE) }; + // Both outcomes compile, so only the tally separates the bridge that + // carries the live red from the one whose first `ec` consumer re-derives + // it off the frame. + crate::trace::fbw_diag::bump(if sym.execution_context.is_none() { + crate::trace::fbw_diag::BRIDGE_EC_MISSING + } else { + crate::trace::fbw_diag::BRIDGE_EC_FROM_PORTAL_RED + }); // pyjitpl.py rebuild_state_after_failure parity: after // a guard failure the tracing-time `virtualizable_boxes` mirror // must be rebuilt from the resume data so subsequent vable @@ -14765,6 +14773,8 @@ pub(crate) fn setup_reconstructed_callee_frame( ctx: &mut TraceCtx, recipe: &ReconstructRecipe, execution_context: *const pyre_interpreter::PyExecutionContext, + ec_box: OpRef, + root_frame_box: OpRef, parent_frames: Vec, ) -> Option<(PendingInlineFrame, Vec)> { let raw_code = recipe.code_ptr as *const pyre_interpreter::CodeObject; @@ -14791,7 +14801,31 @@ pub(crate) fn setup_reconstructed_callee_frame( let w_globals = recover_inline_callee_globals(recipe.code_ptr); let pycode_const = ctx.const_ref(w_code as i64); let w_globals_const = ctx.const_ref(w_globals as i64); - let ec_const = ctx.const_ref(execution_context as i64); + // `PyPyJitDriver.reds = ['frame', 'ec']`: `perform_call` gives an inlined + // callee the caller's own `ec` Box, so the reconstructed frame is seeded + // from the caller's live red. The concrete pointer stays the frame's + // constructor argument, but a ConstPtr built from it would bake the + // recording thread's ExecutionContext into every bridge compiled from this + // loop. + // + // A bridge whose resume data held no value at the portal `ec` color + // arrives with an empty `ec_box` (`bridge_ec_missing`). Read the red off + // the root frame instead, the way `MIFrame::ensure_execution_context` does + // for the opcode walker: a thread owns one ExecutionContext, so every live + // frame's field names the same object. The constant is left only for a + // root that carries no frame OpRef either. + let ec_seed = if !ec_box.is_none() { + ec_box + } else if !root_frame_box.is_none() { + ctx.record_op_with_descr( + majit_ir::OpCode::GetfieldGcR, + &[root_frame_box], + crate::descr::pyframe_execution_context_descr(), + ) + } else { + crate::jitcode_dispatch::census_record("ReconstructedCallee::EcConstFallback"); + ctx.const_ref(execution_context as i64) + }; let locals_boxes: Vec = recipe.registers_r[..nlocals].to_vec(); // This reconstruction path admits freevars but still rejects fresh @@ -14832,7 +14866,7 @@ pub(crate) fn setup_reconstructed_callee_frame( stack_base, pycode_const, w_globals_const, - ec_const, + ec_seed, ); // `perform_call` (`pyjitpl.py`) is three lines — `newframe` + // `setup_call` + `raise ChangeFrame` — and `newframe` (`:2455-2476`) @@ -14985,7 +15019,7 @@ pub(crate) fn setup_reconstructed_callee_frame( // walker-driven callee (an unset ec surfaces as a liveness-active NONE // panic at the first in-callee guard). if pending.sym.execution_context.is_none() { - pending.sym.execution_context = ec_const; + pending.sym.execution_context = ec_seed; } let max_reg = valuestackdepth @@ -14993,7 +15027,7 @@ pub(crate) fn setup_reconstructed_callee_frame( .max(ec_reg as usize + 1); let mut argboxes_r: Vec = vec![OpRef::NONE; max_reg]; argboxes_r[frame_reg as usize] = frame_vable; - argboxes_r[ec_reg as usize] = ec_const; + argboxes_r[ec_reg as usize] = ec_seed; // The recipe's `registers_r` is SEMANTIC-slot-indexed (filled from the // frame vable's `locals_cells_stack_w` array), but the re-executed callee // reads its registers by post-rename COLOR. After stack-slot-pinning diff --git a/pyre/pyre-jit-trace/src/trace.rs b/pyre/pyre-jit-trace/src/trace.rs index ba3a9456a9e..26789dcb473 100644 --- a/pyre/pyre-jit-trace/src/trace.rs +++ b/pyre/pyre-jit-trace/src/trace.rs @@ -1753,6 +1753,8 @@ fn drive_bridge_carrier_walk( let effects_at_entry = crate::jitcode_dispatch::fbw_executed_effect_count(); let root_ec = sym.concrete_execution_context(); + let root_ec_box = sym.execution_context(); + let root_frame_box = sym.frame(); if crate::jitcode_dispatch::p2_diag_enabled() { let pcs: Vec = carrier .recipes @@ -1783,9 +1785,14 @@ fn drive_bridge_carrier_walk( // operand-stack temps; the `_pending` callee sym is unused on the sub-walk // path (the sub-walk drives the callee body off `argboxes_r` + the emitted // frame vable, not a callee MIFrame). - let Some((pending, argboxes_r)) = - crate::state::setup_reconstructed_callee_frame(ctx, recipe, root_ec, Vec::new()) - else { + let Some((pending, argboxes_r)) = crate::state::setup_reconstructed_callee_frame( + ctx, + recipe, + root_ec, + root_ec_box, + root_frame_box, + Vec::new(), + ) else { discard_bridge_carrier_walk(ctx, sym, entry_depth, pre_pos, &pre_virtualref_boxes); crate::jitcode_dispatch::census_record("P2Drain::SetupFailed"); return p2_drain_abort(); @@ -1894,6 +1901,8 @@ fn drive_bridge_carrier_walk( sym, root_pc, root_ec, + root_ec_box, + root_frame_box, &carrier.recipes[i], &carrier.recipes[..i], result, @@ -2178,6 +2187,8 @@ fn drive_middle_frame_and_thread( sym: &mut Sym, root_pc: usize, root_ec: *const pyre_interpreter::PyExecutionContext, + root_ec_box: majit_ir::OpRef, + root_frame_box: majit_ir::OpRef, middle: &majit_metainterp::ReconstructRecipe, paused_parents: &[majit_metainterp::ReconstructRecipe], child_result: majit_ir::OpRef, @@ -2211,9 +2222,14 @@ fn drive_middle_frame_and_thread( crate::jitcode_dispatch::census_record("P2Drain::CtorTailSubstitute"); return Some(instance); } - let Some((pending, middle_argboxes_r)) = - crate::state::setup_reconstructed_callee_frame(ctx, middle, root_ec, Vec::new()) - else { + let Some((pending, middle_argboxes_r)) = crate::state::setup_reconstructed_callee_frame( + ctx, + middle, + root_ec, + root_ec_box, + root_frame_box, + Vec::new(), + ) else { crate::jitcode_dispatch::census_record("P2Drain::MiddleSetupFailed"); return None; }; @@ -6547,6 +6563,42 @@ pub mod fbw_diag { /// Successful multi-frame blackhole adoptions. A fall means the walk /// stopped handing the interpreter an image and went back to legacy replay. pub const BLACKHOLE_ADOPTED_MULTI_FRAME: usize = 13; + /// Admissions the JIT refused before any trace existed, one slot per + /// deciding predicate. Every other counter here describes something the + /// tracer DID; a refused frame or back edge runs interpreted and leaves + /// `loops_compiled`, `loops_aborted` and `guard_failures` all at the value + /// the refusal itself produced, so the population these count is the one + /// blind spot the tally set otherwise has. + /// + /// `GATE_DECLINED_SHAPE` is `unsupported_jit_shape` — a frame the tracer + /// cannot encode or whose resume shape it cannot express. + /// `GATE_DECLINED_FOR_ITER_REGION` is the back edge's FOR_ITER gate, which + /// judges the loop region being entered, so it counts only loops refused + /// for their own bodies. `GATE_DECLINED_FUNCTION_ENTRY` is + /// `function_entry_trace_is_jit_safe`: a trace armed at function entry can + /// reach every FOR_ITER body in the code object, so it is refused for a + /// body no single back edge would have judged. The frame keeps running + /// interpreted and its back edges still decide for themselves, which makes + /// that slot a count of refused traces where the other two count refused + /// entries. + pub const GATE_DECLINED_SHAPE: usize = 14; + pub const GATE_DECLINED_FOR_ITER_REGION: usize = 15; + pub const GATE_DECLINED_FUNCTION_ENTRY: usize = 16; + /// How `setup_bridge_sym` recovered the `ec` red, one slot per outcome, so + /// the two sum to the number of bridge setups. `PyPyJitDriver.reds` names + /// `frame` and `ec`; `frame` comes back through the virtualizable rebuild, + /// but `ec` has no semantic PyFrame slot and is read from its dedicated + /// post-color register in the failing guard's frame-register section. + /// + /// `BRIDGE_EC_FROM_PORTAL_RED` is that read succeeding — the bridge carries + /// the same Box the parent trace did. `BRIDGE_EC_MISSING` is the fallback: + /// a skeleton jitcode has no portal red colors (`u16::MAX`) and a resumed + /// register can be empty, and either way `sym.execution_context` stays + /// `NONE`, so the first consumer re-derives it with a `GetfieldGcR` off the + /// frame. That re-derivation is sound but is not the live red, and nothing + /// else distinguishes the two. + pub const BRIDGE_EC_FROM_PORTAL_RED: usize = 17; + pub const BRIDGE_EC_MISSING: usize = 18; /// The `[jit-stats]` key for each tally slot, in index order, so a slot /// cannot be added without naming it and no reader can print a subset of @@ -6563,9 +6615,11 @@ pub mod fbw_diag { /// `_jit_stats_merged` folds every `[jit-stats]` line into one flat /// `key -> value` map and reads each value as an integer. /// - /// Every key is `fbw_`-prefixed for that same reason: that map is flat and - /// shared with every other counter, so a bare name like `portal_only` - /// collides with whatever else ever picks it. + /// Every key is prefixed for that same reason: that map is flat and shared + /// with every other counter, so a bare name like `portal_only` collides + /// with whatever else ever picks it. The prefix names the producer — + /// `fbw_` for the full-body walk, `gate_` for the admission gates that run + /// before it, `bridge_` for bridge setup. /// /// The length is `RING_BASE` because the tallies are exactly the slots /// below the ring. @@ -6584,6 +6638,11 @@ pub mod fbw_diag { "fbw_store_journal_rollback_failed", "fbw_blackhole_adopted_single_frame", "fbw_blackhole_adopted_multi_frame", + "gate_declined_shape", + "gate_declined_for_iter_region", + "gate_declined_function_entry", + "bridge_ec_from_portal_red", + "bridge_ec_missing", ]; /// One ring entry per walk: four slots of outcome name (8 ASCII bytes per @@ -6593,7 +6652,7 @@ pub mod fbw_diag { /// /// `pyre-wasm-runner` decodes the ring through its OWN copy of this /// constant (`main.rs`); the two have to move together. - pub const RING_BASE: usize = 14; + pub const RING_BASE: usize = 19; pub const RING_ENTRIES: usize = 24; pub const RING_STRIDE: usize = 5; pub const NAME_SLOTS: usize = 4; @@ -6621,6 +6680,21 @@ pub mod fbw_diag { FBW_DIAG[i].fetch_add(1, Ordering::Relaxed); } + /// Record one refused admission. The gates that decide these run in + /// `pyre-jit`, so they reach the tallies through named entry points rather + /// than a slot index no caller outside this module should be spelling. + pub fn record_gate_declined_shape() { + bump(GATE_DECLINED_SHAPE); + } + + pub fn record_gate_declined_for_iter_region() { + bump(GATE_DECLINED_FOR_ITER_REGION); + } + + pub fn record_gate_declined_function_entry() { + bump(GATE_DECLINED_FUNCTION_ENTRY); + } + /// Read one slot (out-of-range reads as 0). Surfaced to the wasm host /// through the `pyre_fbw_diag` export in the `pyre-wasm` crate. pub fn get(i: usize) -> u64 { diff --git a/pyre/pyre-jit-trace/tests/fbw_diag_mirror.rs b/pyre/pyre-jit-trace/tests/fbw_diag_mirror.rs index 6fee5aa17c1..034cdda47ea 100644 --- a/pyre/pyre-jit-trace/tests/fbw_diag_mirror.rs +++ b/pyre/pyre-jit-trace/tests/fbw_diag_mirror.rs @@ -247,6 +247,17 @@ fn each_label_sits_at_its_own_slot_constant() { d::BLACKHOLE_ADOPTED_MULTI_FRAME, "fbw_blackhole_adopted_multi_frame", ), + (d::GATE_DECLINED_SHAPE, "gate_declined_shape"), + ( + d::GATE_DECLINED_FOR_ITER_REGION, + "gate_declined_for_iter_region", + ), + ( + d::GATE_DECLINED_FUNCTION_ENTRY, + "gate_declined_function_entry", + ), + (d::BRIDGE_EC_FROM_PORTAL_RED, "bridge_ec_from_portal_red"), + (d::BRIDGE_EC_MISSING, "bridge_ec_missing"), ]; // Exhaustiveness: a slot added to `fbw_diag` grows `LABELS` (rustc enforces diff --git a/pyre/pyre-jit/src/eval.rs b/pyre/pyre-jit/src/eval.rs index d20669f199b..0848ae7a10d 100644 --- a/pyre/pyre-jit/src/eval.rs +++ b/pyre/pyre-jit/src/eval.rs @@ -7733,6 +7733,39 @@ fn for_iter_body_is_jit_safe_at(code: &pyre_interpreter::CodeObject, pc: usize) | I::ListExtend { .. } | I::SetAdd { .. } | I::MapAdd { .. } + // The other three container-update opcodes lower the same + // way as `LIST_EXTEND`: pop the source, peek the container, + // emit one void accumulate residual, and the codewriter and + // `liveness` already treat all four as one class. Each also + // targets a container built in the same expression -- a set + // or dict display, or a call's `**kwargs` dict -- so a walk + // abort drops an incomplete fresh object rather than + // replaying a mutation of a pre-existing one. A set display + // that must yield a mutable set compiles to `BUILD_SET 0` + + // `SET_UPDATE 1` even when every element is constant, so + // declining these took every loop that builds one out of + // the JIT. + | I::SetUpdate { .. } + | I::DictUpdate { .. } + | I::DictMerge { .. } + // `LOAD_BUILD_CLASS` pushes `frame.get_builtin()` and + // touches nothing else. `codewriter.rs` lowers it as a + // frame-only Ref read under a compile-time assert that + // `HONOR_BUILTINS` is false, so which frame asks does not + // change the answer. A loop body that defines a class + // holds one. + // + // `DELETE_NAME` and `DELETE_GLOBAL` deliberately stay out, + // even though they reach the same + // `try_walker_force_quasi_immut_namespace_write` as the + // two stores above. A repeated store settles: after the + // first write `store_would_bump_version` stops bumping and + // the loop compiles. A delete removes the cell, so it + // bumps every iteration and forces every iteration -- + // `x = i * 2; total += x` compiles with 0 aborts, and the + // same loop with `del x` appended traces 5 times and + // aborts all 5 on the force, compiling nothing. + | I::LoadBuildClass ) || ((!body_has_call || for_iter_call_body_admitted()) && matches!(body_instr, I::ListAppend { .. })); @@ -8264,6 +8297,7 @@ fn eval_with_jit_inner( UnsupportedJitShape::CurrentFrameOnly | UnsupportedJitShape::NestedBreakBridgeResume | UnsupportedJitShape::ConstEncodingOverflow => { + pyre_jit_trace::trace::fbw_diag::record_gate_declined_shape(); pyre_jit_trace::jitcode_dispatch::census_record_frame_shape_decline( code as *const _ as usize, unsupported_jit_shape(code).1, @@ -9160,9 +9194,11 @@ fn maybe_compile_and_run( // whole-bytecode walk that used to run on every back-edge. let code = unsafe { &*pyre_interpreter::pyframe_get_pycode(frame) }; if cached_unsupported_jit_shape(code) != UnsupportedJitShape::None { + pyre_jit_trace::trace::fbw_diag::record_gate_declined_shape(); return None; } if !cached_loop_region_for_iter_bodies_all_jit_safe(code, loop_header_pc) { + pyre_jit_trace::trace::fbw_diag::record_gate_declined_for_iter_region(); const DENIAL: &str = "BackedgeGate::ForIter/UnsafeLoopRegion"; let first_decline = pyre_jit_trace::jitcode_dispatch::census_record_for_iter_gate_decline( code as *const _ as usize, @@ -10573,6 +10609,7 @@ pub fn try_function_entry_jit(frame: &mut PyFrame) -> Option { // object in `CallControl.graph_jit_shapes`, so this is a pointer-keyed // lookup, not the whole-frame scan that charged every Python call. if cached_unsupported_jit_shape(code) != UnsupportedJitShape::None { + pyre_jit_trace::trace::fbw_diag::record_gate_declined_shape(); return None; } if dump_bytecode_enabled() { @@ -10852,6 +10889,7 @@ pub fn try_function_entry_jit(frame: &mut PyFrame) -> Option { // continues in `eval_loop_jit`, where its back-edges tick independently // and consult their own natural loop regions. if !cached_function_entry_trace_is_jit_safe(code) { + pyre_jit_trace::trace::fbw_diag::record_gate_declined_function_entry(); return None; } @@ -13731,6 +13769,73 @@ mod tests { } } + #[test] + fn for_iter_body_updating_a_fresh_container_is_jit_safe() { + // `SET_UPDATE`, `DICT_UPDATE` and `DICT_MERGE` lower like `LIST_EXTEND` + // -- one void accumulate residual over a container the same expression + // just built. A set display that must yield a mutable set emits + // `BUILD_SET 0` + `SET_UPDATE 1` even with constant elements, so + // declining it took the whole loop out of the JIT. + use pyre_interpreter::compile_exec; + for source in [ + "def f(n):\n c = 0\n for i in range(n):\n s = {0, 1, 2, 3}\n c += len(s)\n return c\n", + "def f(n):\n c = 0\n for i in range(n):\n d = {**{'a': 1}, 'b': i}\n c += len(d)\n return c\n", + "def f(n, g):\n c = 0\n for i in range(n):\n c += g(**{'a': i})\n return c\n", + ] { + let module = compile_exec(source).expect("test code should compile"); + let code = function_code_from_module(&module, "f"); + assert!(function_entry_trace_is_jit_safe(&code)); + assert_eq!(unsupported_jit_shape_of(&code), UnsupportedJitShape::None); + } + } + + #[test] + fn for_iter_body_defining_a_class_is_jit_safe() { + use pyre_interpreter::compile_exec; + let module = compile_exec( + "def f(n):\n c = 0\n for i in range(n):\n class C:\n v = i\n c += C.v\n return c\n", + ) + .expect("test code should compile"); + let code = function_code_from_module(&module, "f"); + assert!(function_entry_trace_is_jit_safe(&code)); + assert_eq!(unsupported_jit_shape_of(&code), UnsupportedJitShape::None); + } + + #[test] + fn for_iter_body_deleting_a_namespace_name_is_refused() { + // The counterpart to the `STORE_NAME` and `STORE_GLOBAL` admission: a + // store settles after its first version bump, a delete bumps on every + // iteration and forces the trace every time, so the loop it would + // admit never compiles. Only a module or class frame spells + // `DELETE_NAME` -- a function's `del x` is `DELETE_FAST`, which stays + // admitted, so the store-only loop below has to keep passing. + use pyre_interpreter::compile_exec; + let stores = + compile_exec("total = 0\nfor i in range(10):\n x = i * 2\n total += x\n") + .expect("test code should compile"); + assert!(function_entry_trace_is_jit_safe(&stores)); + + let deletes = compile_exec( + "total = 0\nfor i in range(10):\n x = i * 2\n total += x\n del x\n", + ) + .expect("test code should compile"); + assert!(!function_entry_trace_is_jit_safe(&deletes)); + + let del_global = compile_exec( + "g = 0\ndef f(n):\n global g\n c = 0\n for i in range(n):\n g = i\n c += g\n del g\n return c\n", + ) + .expect("test code should compile"); + let code = function_code_from_module(&del_global, "f"); + assert!(!function_entry_trace_is_jit_safe(&code)); + + let del_fast = compile_exec( + "def f(n):\n c = 0\n for i in range(n):\n x = i * 2\n c += x\n del x\n return c\n", + ) + .expect("test code should compile"); + let code = function_code_from_module(&del_fast, "f"); + assert!(function_entry_trace_is_jit_safe(&code)); + } + #[test] fn for_iter_flat_arithmetic_body_is_jit_safe() { use pyre_interpreter::compile_exec; diff --git a/pyre/pyre-wasm-runner/src/main.rs b/pyre/pyre-wasm-runner/src/main.rs index f2603758448..adf448db569 100644 --- a/pyre/pyre-wasm-runner/src/main.rs +++ b/pyre/pyre-wasm-runner/src/main.rs @@ -810,7 +810,7 @@ fn run(module_path: &Path, source: &str, script: &Path) -> Result { // PYRE_WASM_JIT_STATS, so printing them here would have kept them off // every gated wasm run. if let Ok(fbw) = instance.get_typed_func::(&mut store, "pyre_fbw_diag") { - const RING_BASE: u32 = 14; + const RING_BASE: u32 = 19; const RING_ENTRIES: u32 = 24; const RING_STRIDE: u32 = 5; const NAME_SLOTS: u32 = 4; @@ -1073,7 +1073,8 @@ fn run(module_path: &Path, source: &str, script: &Path) -> Result { let field_pos_parent_empty = counter("pyre_jit_field_pos_parent_empty", &mut missing); let field_pos_rederived = counter("pyre_jit_field_pos_rederived", &mut missing); let field_pos_unresolved = counter("pyre_jit_field_pos_unresolved", &mut missing); - // Every full-body-walk tally, reached through the slot-indexed + // Every tally below the ring — the full-body-walk counts and the + // admission-gate refusals — reached through the slot-indexed // `pyre_fbw_diag` export rather than a counter export of its own. // // POSITIONAL MIRROR of `pyre_jit_trace::trace::fbw_diag::LABELS` — the @@ -1107,7 +1108,7 @@ fn run(module_path: &Path, source: &str, script: &Path) -> Result { // Both arrays take their length from this one constant, so a label // added without a slot (or the reverse) is a compile error rather than // a `zip` that silently drops the tail. - const FBW_SLOTS: usize = 14; + const FBW_SLOTS: usize = 19; let fbw_labels: [&str; FBW_SLOTS] = [ "fbw_walks", "fbw_rolled_back_with_effects", @@ -1123,6 +1124,11 @@ fn run(module_path: &Path, source: &str, script: &Path) -> Result { "fbw_store_journal_rollback_failed", "fbw_blackhole_adopted_single_frame", "fbw_blackhole_adopted_multi_frame", + "gate_declined_shape", + "gate_declined_for_iter_region", + "gate_declined_function_entry", + "bridge_ec_from_portal_red", + "bridge_ec_missing", ]; let fbw_slots = match instance .get_typed_func::(&mut store, "pyre_fbw_diag")