diff --git a/crates/perry-codegen/src/lower_call/method_override.rs b/crates/perry-codegen/src/lower_call/method_override.rs index d45b8747b1..04c29b1240 100644 --- a/crates/perry-codegen/src/lower_call/method_override.rs +++ b/crates/perry-codegen/src/lower_call/method_override.rs @@ -213,6 +213,19 @@ fn total_value_truthy(ctx: &mut FnCtx<'_>, value: &str) -> String { /// and its exact `(class_id, ShapeId)` pair still matches the /// compiler-published pair. Any failed proof takes the unchanged dynamic /// method fallback. +/// Kill switch for the probe-before-runtime-guard emission +/// (`PERRY_METHOD_INLINE_PROBE=0` restores the guard-first form for A/B). +fn method_inline_probe_enabled() -> bool { + use std::sync::OnceLock; + static CACHED: OnceLock = OnceLock::new(); + *CACHED.get_or_init(|| { + !matches!( + std::env::var("PERRY_METHOD_INLINE_PROBE").as_deref(), + Ok("0") | Ok("off") | Ok("false") + ) + }) +} + pub(crate) fn emit_inline_direct_method_shape_guard( ctx: &mut FnCtx<'_>, recv_box: &str, @@ -979,6 +992,22 @@ pub(super) fn emit_guarded_direct_method_call( // single-arm sites retain the runtime helper. let multi_arm = !subclass_arms.is_empty(); let inline_single_arm = shape_only_guard && !multi_arm; + // #9105's dispensation, applied to METHOD sites: normal builds do not + // collect typed feedback, so the runtime guard's observation half is + // inert — yet every monomorphic hit still paid its full contract check + // (an RwLock read plus TWO SipHash HashMap probes in + // `vtable_method_matches`, ~180-390 ns/call on a typed-param receiver). + // Decide the monomorphic case with the SAME inline probe the shape-only + // sites emit — its prototype-override latches are exactly what the + // shape-only direct dispatch already trusts for calling this method + // body — and keep the out-of-line guard (which records the observation + // and handles forwarding/exotic receivers) as the probe-MISS edge, so + // nothing is lost. Emission-enabled builds keep the guard first so the + // feedback stream still sees every call. + let probe_before_runtime_guard = !shape_only_guard + && !multi_arm + && !crate::expr::typed_feedback_emission_enabled() + && method_inline_probe_enabled(); if multi_arm { let (cid, shape_id) = emit_inline_direct_method_shape_probe(ctx, recv_box, &method_guard_slot_str); @@ -1018,6 +1047,20 @@ pub(super) fn emit_guarded_direct_method_call( &fallback_label, ); } + if probe_before_runtime_guard { + let runtime_guard_idx = ctx.new_block("method_direct.runtime_guard"); + let runtime_guard_label = ctx.block_label(runtime_guard_idx); + emit_inline_direct_method_shape_guard( + ctx, + recv_box, + &expected_class_id_str, + &expected_shape_id, + &method_guard_slot_str, + &fast_label, + &runtime_guard_label, + ); + ctx.current_block = runtime_guard_idx; + } let guard_ok = if multi_arm || inline_single_arm { // The chain above already terminated the guard block and every test // block, or the inline single-arm guard terminated both its pointer diff --git a/crates/perry-codegen/tests/native_proof_regressions.rs b/crates/perry-codegen/tests/native_proof_regressions.rs index 52537c7222..dded0ed8fc 100644 --- a/crates/perry-codegen/tests/native_proof_regressions.rs +++ b/crates/perry-codegen/tests/native_proof_regressions.rs @@ -12648,6 +12648,14 @@ fn typed_f64_receiver_method_clone_raw_loads_after_composed_guards() { let method_guard = caller_ir .find("call i32 @js_typed_feedback_method_direct_call_guard") .unwrap_or_else(|| panic!("caller should use the full method-direct guard:\n{caller_ir}")); + // The method-direct PROOF that dominates the fast arm is the inline + // shape probe (its first block loads the prototype-override latch) — + // the runtime guard is that probe's miss edge and is emitted after the + // fast arm in text order. Either form is the same proof; take whichever + // comes first so the ordering assertion below is about the proof, not + // about which emission shape carried it. + let inline_probe = caller_ir.find("@PERRY_CLASS_PROTOTYPE_FAST_GUARDS_INVALIDATED"); + let method_proof = inline_probe.map_or(method_guard, |p| p.min(method_guard)); let field_guard = caller_ir .find("call i32 @js_typed_feedback_class_field_get_guard") .unwrap_or_else(|| panic!("caller should guard raw-f64 receiver fields:\n{caller_ir}")); @@ -12655,7 +12663,7 @@ fn typed_f64_receiver_method_clone_raw_loads_after_composed_guards() { .find(&format!("call double @{typed}(i64 ")) .unwrap_or_else(|| panic!("caller should call the receiver clone:\n{caller_ir}")); assert!( - method_guard < field_guard && field_guard < typed_call, + method_proof < field_guard && field_guard < typed_call, "receiver clone must run only after method-direct and raw-f64 field guards:\n{caller_ir}" ); // #7506: this used to assert the guard-failure edge calls `$generic` BY diff --git a/crates/perry/src/commands/compile/build_cache.rs b/crates/perry/src/commands/compile/build_cache.rs index 4ea4da94a2..41e5c17814 100644 --- a/crates/perry/src/commands/compile/build_cache.rs +++ b/crates/perry/src/commands/compile/build_cache.rs @@ -57,6 +57,11 @@ const BUILD_CACHE_ENV_VARS: &[&str] = &[ // off, the map is empty and every binding takes the entry-resolved // indirect path, so the two settings emit different call sequences. "PERRY_CALL_DEVIRT", + // #9124: gates deciding the monomorphic method case with the inline + // shape probe instead of the typed-feedback runtime guard — the two + // settings emit different call sequences, so a cached object from one + // must not serve the other. + "PERRY_METHOD_INLINE_PROBE", // #9060: gates whether a reduce accumulator earns the stable-packed fast // clone's numeric proof — with it on, `s += arr[i]` lowers to an inline // fadd instead of `js_dynamic_string_or_number_add`, so the two settings