Skip to content

fix(hir): brand-check typed Number/Boolean prototype .call (#4100)#4124

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4100-typed-call-brand
Jun 2, 2026
Merged

fix(hir): brand-check typed Number/Boolean prototype .call (#4100)#4124
proggeramlug merged 1 commit into
mainfrom
worktree-fix-4100-typed-call-brand

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

#4100 residual — typed .call on Number/Boolean prototype methods

#4112 fixed the reflective (as any / computed-key) form of the
Number/Boolean/Symbol/BigInt prototype brand checks, but a residual remained
for the statically-typed member-access form, verified on main:

Number.prototype.valueOf.call({})    // Perry: "[object Object]"   Node: TypeError
Number.prototype.toString.call({})   // Perry: "[object Object]"   Node: TypeError
Boolean.prototype.valueOf.call({})   // Perry: "[object Object]"   Node: TypeError
const v = Number.prototype.valueOf; v.call({})  // same gap

Root cause

try_builtin_prototype_method_apply_call (HIR) folds a typed
<recv>.<method>.call(x) into x.<method>(). That routes through the lenient
codegen fast-path / Object.prototype fallback and never reaches the
brand-check thunk installed by #4112. The as any / computed-key forms aren't
folded, so they already throw correctly.

Fix

Guard the fold (and the const-extracted-local fold in
as_builtin_proto_method_ref) so Number.prototype's
valueOf/toString/toLocaleString and Boolean.prototype's
valueOf/toString stay reflective and hit the thunk — mirroring the existing
Function.prototype.toString guard (#4101).

toFixed/toExponential/toPrecision are deliberately not guarded: the
fold is the correct path for them (their reflective dispatch over-throws on a
valid receiver), and only the brand-checked methods are affected. Symbol/BigInt
have no codegen fold path, so they need no guard.

Verification

  • test-files/test_gap_4100_primitive_proto_brand_check.ts extended with the
    typed-.call/.apply, extracted-local, valid-receiver, and toFixed/toExponential
    no-regression cases.
  • Byte-identical to node --experimental-strip-types in both auto-optimize and
    PERRY_NO_AUTO_OPTIMIZE modes.
  • Array/String/Function .call folds unchanged; cargo test -p perry-hir green.

Closes #4100.

#4112 fixed the reflective (as-any / computed-key) form of
Number/Boolean/Symbol/BigInt prototype brand checks, but a residual
remained for the statically-typed member-access form:

    Number.prototype.valueOf.call({})   // returned "[object Object]"
    Boolean.prototype.toString.call({}) // instead of throwing TypeError

The typed `.call`/`.apply` was folded by
try_builtin_prototype_method_apply_call into `x.<method>()`, routing
through the lenient codegen fast-path / Object.prototype fallback and
bypassing the installed brand-check thunk.

Guard the fold (and the const-extracted-local fold in
as_builtin_proto_method_ref) for Number.prototype's
valueOf/toString/toLocaleString and Boolean.prototype's
valueOf/toString so they stay reflective and hit the thunk — mirroring
the existing Function.prototype.toString guard (#4101).

toFixed/toExponential/toPrecision are deliberately NOT guarded: the fold
is the correct path for them (their reflective dispatch over-throws on a
valid receiver), and only the brand-checked methods are affected.
Symbol/BigInt have no codegen fold path, so they need no guard.

Verified byte-identical to node --experimental-strip-types in both
auto-optimize and PERRY_NO_AUTO_OPTIMIZE modes.
@proggeramlug proggeramlug merged commit 565be71 into main Jun 2, 2026
11 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-4100-typed-call-brand branch June 2, 2026 15:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

runtime: Number/Boolean/Symbol/BigInt prototype methods don't brand-check this (reflective call returns [object Object] instead of TypeError) (#3662)

1 participant