You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
runtime: Number/Boolean/Symbol/BigInt prototype methods don't brand-check this (reflective call returns [object Object] instead of TypeError) (#3662) #4100
Part of the #3662 cluster (builtins must throw the spec error on an incompatible this).
The primitive-wrapper prototype methods do not perform the spec this brand
check when invoked reflectively on an incompatible receiver. Instead the call
falls back to Object.prototype and returns "[object Object]" rather than
throwing a TypeError.
Affected (each should throw TypeError on a non-matching receiver):
Install dedicated brand-checking thunks on Number/Boolean/Symbol/BigInt .prototype (mirror object/collection_proto_thunks.rs): read IMPLICIT_THIS,
verify it is the matching primitive (or its boxed wrapper), throw TypeError
otherwise, and re-dispatch to the existing per-type logic via js_native_call_method — which also fixes the wrong-value reflective toString.
I am taking this with #4099 and #4102 in one runtime reflective-builtins PR cut. Planned scope: fix reflective collection set receiver validation, primitive wrapper prototype brand/value dispatch, and dynamic instanceof / constructor @@hasInstance recognition for Array/Object/Date without taking the separate function-source or typed-array-view work.
Investigation note (saves the next implementer a dead-end):
The obvious fix — install brand-checking thunks on Number.prototype / Boolean.prototype in global_this::populate_builtin_prototype_methods (the
exact recipe that works for the collection prototypes in collection_proto_thunks.rs) — does not work here.
Confirmed empirically:
populate_builtin_prototype_methods("Number"/"Boolean", proto)is called
(lazily, when the globalThis builtin singleton is first built), and the
thunks install onto that singleton prototype object.
But reflective Number.prototype.valueOf.call({}) still returns "[object Object]" and never reaches the installed thunk — even after the
singleton has been populated.
So Number.prototype.valueOf does not read its valueOf from the
singleton-populated prototype the way Map.prototype.set does. It resolves the
method via a different path (it ends up at the shared global_this_builtin_noop_thunk, whose reflective .call re-dispatches by name onto the receiver — js_native_call_method({}, "valueOf") → falls
through to Object.prototype.valueOf → [object Object]).
The real fix needs interception at the actual reflective-dispatch site, e.g. try_dispatch_value_called_proto_method / the value-called-proto handling in native_call_method.rs, with enough context to know the method came off Number/Boolean/Symbol/BigInt.prototype (the noop currently loses that
origin), or making Number.prototype resolve to the populated singleton
prototype. This is a deeper change than the collection recipe, hence deferred.
Scope reminder for this issue: Number.prototype.{valueOf,toLocaleString,
toFixed,toString,toExponential,toPrecision}, Boolean.prototype.{toString,
valueOf}, Symbol.prototype.{toString,valueOf}, BigInt.prototype.{toString,
valueOf} on an incompatible receiver must throw TypeError; the same fix also
corrects reflective Symbol/BigInt toString returning [object Symbol] / [object BigInt] (should be Symbol(x) / the radix string).
Reopening — #4112 fixed most of this (computed-key access + Symbol/BigInt + the wrong-value reflective toString), but a residual remains for the direct / extracted member-access form of Number.prototype.* and Boolean.prototype.*. Verified on main (d990b021e):
// computed access — correctly throws (fixed by #4112)(Number.prototypeasany)["valueOf"].call({})// TypeError ✓// direct member access — still NOT brand-checkedNumber.prototype.valueOf.call({})// Perry: "[object Object]" Node: TypeErrorNumber.prototype.toString.call({})// Perry: "[object Object]" Node: TypeErrorBoolean.prototype.valueOf.call({})// Perry: "[object Object]" Node: TypeError// extraction has the same gapconstv=Number.prototype.valueOf;v.call({})// Perry: "[object Object]" Node: TypeError
Symbol/BigInt direct access does throw (no codegen fast-path), so this is specific to Number/Boolean. The direct Number.prototype.<m> read is lowered by codegen to a path that resolves the method without going through the installed brand-check thunk (it ends up at the Object.prototype fallback), whereas the computed ["<m>"] read does a generic property get that hits the thunk. The fix is to make the direct-member Number.prototype.<m> / Boolean.prototype.<m> value read resolve to the same installed thunk (or route the reflective dispatch through the brand check regardless of access form).
(Same direct-vs-computed split was visible in the original triage; #4112's primitive_proto_thunks only intercepts the computed/generic path.)
Correction to the root cause above — the HIR for the direct and computed property reads is identical, so it's not a property-read divergence. The real trigger is the statically-typed .call lowering:
Number.prototype.valueOf.call({})// Perry: "[object Object]" Node: TypeError(Number.prototypeasany).valueOf.call({})// throws ✓ (as-any on receiver)(Number.prototype.valueOfasany).call({})// throws ✓ (as-any on method)Number.prototype.valueOf.call({}asany)// "[object Object]" (as-any on arg — no effect)
When Number.prototype.valueOf is statically typed (TS knows it as () => number), codegen lowers the .call(...) through a typed/optimized dispatch path that resolves the method without going through the installed primitive_proto_thunks brand-check. Erasing the type with as any on the receiver or the method drops it to the dynamic js_native_call_method path, which hits the thunk and throws correctly.
So #4112's thunks are correct; the remaining gap is that the typed-.call codegen fast-path for Number/Boolean prototype methods bypasses them. This is a very niche reflective pattern (TS would normally reject the receiver-type mismatch anyway), so low priority — but tracking it for completeness.
Activity
andrewtdiz commentedon Jun 2, 2026
I am taking this with #4099 and #4102 in one runtime reflective-builtins PR cut. Planned scope: fix reflective collection set receiver validation, primitive wrapper prototype brand/value dispatch, and dynamic instanceof / constructor @@hasInstance recognition for Array/Object/Date without taking the separate function-source or typed-array-view work.
proggeramlug commentedon Jun 2, 2026
Investigation note (saves the next implementer a dead-end):
The obvious fix — install brand-checking thunks on
Number.prototype/Boolean.prototypeinglobal_this::populate_builtin_prototype_methods(theexact recipe that works for the collection prototypes in
collection_proto_thunks.rs) — does not work here.Confirmed empirically:
populate_builtin_prototype_methods("Number"/"Boolean", proto)is called(lazily, when the globalThis builtin singleton is first built), and the
thunks install onto that singleton prototype object.
Number.prototype.valueOf.call({})still returns"[object Object]"and never reaches the installed thunk — even after thesingleton has been populated.
So
Number.prototype.valueOfdoes not read itsvalueOffrom thesingleton-populated prototype the way
Map.prototype.setdoes. It resolves themethod via a different path (it ends up at the shared
global_this_builtin_noop_thunk, whose reflective.callre-dispatches byname onto the receiver —
js_native_call_method({}, "valueOf")→ fallsthrough to
Object.prototype.valueOf→[object Object]).The real fix needs interception at the actual reflective-dispatch site, e.g.
try_dispatch_value_called_proto_method/ the value-called-proto handling innative_call_method.rs, with enough context to know the method came offNumber/Boolean/Symbol/BigInt.prototype (the noop currently loses thatorigin), or making
Number.prototyperesolve to the populated singletonprototype. This is a deeper change than the collection recipe, hence deferred.
Scope reminder for this issue: Number.prototype.{valueOf,toLocaleString,
toFixed,toString,toExponential,toPrecision}, Boolean.prototype.{toString,
valueOf}, Symbol.prototype.{toString,valueOf}, BigInt.prototype.{toString,
valueOf} on an incompatible receiver must throw
TypeError; the same fix alsocorrects reflective Symbol/BigInt
toStringreturning[object Symbol]/[object BigInt](should beSymbol(x)/ the radix string).fix(runtime): brand-check primitive-wrapper prototype methods (#4100)
fix(runtime): brand-check primitive-wrapper prototype methods (#4100) (…
andrewtdiz commentedon Jun 2, 2026
Draft PR opened for this: #4115
proggeramlug commentedon Jun 2, 2026
Reopening — #4112 fixed most of this (computed-key access + Symbol/BigInt + the wrong-value reflective
toString), but a residual remains for the direct / extracted member-access form ofNumber.prototype.*andBoolean.prototype.*. Verified onmain(d990b021e):Symbol/BigInt direct access does throw (no codegen fast-path), so this is specific to
Number/Boolean. The directNumber.prototype.<m>read is lowered by codegen to a path that resolves the method without going through the installed brand-check thunk (it ends up at theObject.prototypefallback), whereas the computed["<m>"]read does a generic property get that hits the thunk. The fix is to make the direct-memberNumber.prototype.<m>/Boolean.prototype.<m>value read resolve to the same installed thunk (or route the reflective dispatch through the brand check regardless of access form).(Same direct-vs-computed split was visible in the original triage; #4112's
primitive_proto_thunksonly intercepts the computed/generic path.)proggeramlug commentedon Jun 2, 2026
Correction to the root cause above — the HIR for the direct and computed property reads is identical, so it's not a property-read divergence. The real trigger is the statically-typed
.calllowering:When
Number.prototype.valueOfis statically typed (TS knows it as() => number), codegen lowers the.call(...)through a typed/optimized dispatch path that resolves the method without going through the installedprimitive_proto_thunksbrand-check. Erasing the type withas anyon the receiver or the method drops it to the dynamicjs_native_call_methodpath, which hits the thunk and throws correctly.So #4112's thunks are correct; the remaining gap is that the typed-
.callcodegen fast-path forNumber/Booleanprototype methods bypasses them. This is a very niche reflective pattern (TS would normally reject the receiver-type mismatch anyway), so low priority — but tracking it for completeness.fix(hir): brand-check typed Number/Boolean prototype .call (#4100) (#…
fix(runtime): avoid primitive-method recursion via String.prototype r…
[codex] Align primitive wrapper receivers (#4161)
fix(runtime,hir): observable spec-order argument coercion for reflect…
fix(runtime,hir): observable spec-order argument coercion for reflect…