Skip to content

A guarded $spec_b clone never re-enters itself either — the Tier-B half of #8167 #8169

Description

@proggeramlug

#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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions