#8167 fixed the raw-i32 half of "a specialized clone never re-enters itself". The Tier-B ($spec_b) half is untouched and is the shape most real code hits, because it does not need a literal call site to exist.
Change fib40.ts's entry call from a literal to a variable and the same function takes the guarded route:
function fib(n: number): number { return n < 2 ? n : fib(n - 1) + fib(n - 2); }
const k = Number(process.argv[2] ?? "30");
console.log(fib(k));
--trace llvm on perf/spec-clone-self-recursion (i.e. WITH #8167):
define internal double @…__fib$spec_b(double %arg1) {
%r3 = fcmp olt double %r2, 2.0 ; the clone is fast
%r9 = fsub double %r8, 1.0
%r10 = call double @…__fib(double %r9) ; …but re-enters the PUBLIC TRAMPOLINE
%r13 = call double @…__fib(double %r12)
}
define double @…__fib(double %arg1) noinline { … js_param_type_guard … }
So every recursive step pays a noinline trampoline call plus a js_param_type_guard runtime call, then lands back in the clone it just left.
Why it happens
try_emit_spec_guarded_call routes a site only when guarded_argument_proves(ctx, arg, expected) holds, and guarded_path_type has no arm for Expr::Binary — so a derived argument (n - 1) yields None and the site keeps the public wrapper. Identical in structure to the i32 defect #8167 fixed, one tier over.
Why the fix should be cheaper than #8167's
A Boxed slot takes the double verbatim — a JS Number's NaN-box is its double. There is no conversion, so none of #8167's three obligations apply: no 32-bit range test, no exact-integer window, no -0 hazard. The whole obligation is "this value is a Number", which the codebase already has a predicate for (type_analysis::is_numeric_expr — the same one that gates fadd versus js_dynamic_add, so if it were wrong arithmetic would already be miscompiling).
Sketch: in guarded_path_type (crates/perry-codegen/src/lower_call/func_ref.rs), fall back to Type::Number when is_numeric_expr(ctx, expr) holds and no structural arm matched.
Why it was NOT done in #8167
guarded_path_type also feeds guarded_expr_proof and guarded_call_return_proof, so widening it widens more than the call-site router, and #8167's owner-set scope was explicitly "fix fib40's mechanism, do not claim the other eight regressed rows without measuring each". This wants its own A/B of cargo test -p perry-codegen --no-fail-fast (by name) and its own corpus measurement — several of the still-regressed rows (tree, interp, shapes) have number-typed hot parameters with no literal call site, which is exactly this shape.
#8167 fixed the raw-
i32half of "a specialized clone never re-enters itself". The Tier-B ($spec_b) half is untouched and is the shape most real code hits, because it does not need a literal call site to exist.Change
fib40.ts's entry call from a literal to a variable and the same function takes the guarded route:--trace llvmonperf/spec-clone-self-recursion(i.e. WITH #8167):So every recursive step pays a
noinlinetrampoline call plus ajs_param_type_guardruntime call, then lands back in the clone it just left.Why it happens
try_emit_spec_guarded_callroutes a site only whenguarded_argument_proves(ctx, arg, expected)holds, andguarded_path_typehas no arm forExpr::Binary— so a derived argument (n - 1) yieldsNoneand the site keeps the public wrapper. Identical in structure to thei32defect #8167 fixed, one tier over.Why the fix should be cheaper than #8167's
A
Boxedslot takes thedoubleverbatim — a JS Number's NaN-box is its double. There is no conversion, so none of #8167's three obligations apply: no 32-bit range test, no exact-integer window, no-0hazard. The whole obligation is "this value is a Number", which the codebase already has a predicate for (type_analysis::is_numeric_expr— the same one that gatesfaddversusjs_dynamic_add, so if it were wrong arithmetic would already be miscompiling).Sketch: in
guarded_path_type(crates/perry-codegen/src/lower_call/func_ref.rs), fall back toType::Numberwhenis_numeric_expr(ctx, expr)holds and no structural arm matched.Why it was NOT done in #8167
guarded_path_typealso feedsguarded_expr_proofandguarded_call_return_proof, so widening it widens more than the call-site router, and #8167's owner-set scope was explicitly "fixfib40's mechanism, do not claim the other eight regressed rows without measuring each". This wants its own A/B ofcargo test -p perry-codegen --no-fail-fast(by name) and its own corpus measurement — several of the still-regressed rows (tree,interp,shapes) havenumber-typed hot parameters with no literal call site, which is exactly this shape.