Repository navigation
User methods named like Date/Number/Array built-ins (getTime, toFixed, toISOString, toSorted, …) are compiled as the built-in regardless of the receiver — the user method never runs #10476
Description
Found by the package audit (compiling real npm packages from source instead of Perry's native bindings) on
Perry e6dcb62 (v0.5.1587), Linux x64. A method call whose NAME matches a Date.prototype, Number.prototype or
some Array.prototype methods is lowered to that built-in by name, whatever the receiver is. The user's method is never
called: you get NaN, [object Object], Invalid Date or RangeError: Invalid time value instead.
Reproduction
package.json
{"name":"repro","private":true,"type":"module"}dayjs_like.js (dayjs 1.11.x toJSON shape)
function Dayjs(ms) { this.$ms = ms; }
Dayjs.prototype.toISOString = function () { return 'Dayjs.toISOString(' + this.$ms + ')'; };
Dayjs.prototype.toJSON = function () { return this.toISOString(); };
try { console.log('js this.toISOString() in toJSON', new Dayjs(0).toJSON()); } catch (e) { console.log('js this.toISOString() in toJSON THREW', String(e)); }main.ts
import './dayjs_like.js';
const t = (label: string, f: () => unknown) => { try { console.log(label, String(f())); } catch (e) { console.log(label, 'THREW', String(e)); } };
function Money(this: any, v: number) { this.v = v; }
Money.prototype.toFixed = function (d: number) { return 'Money.toFixed'; };
Money.prototype.getTime = function () { return 'Money.getTime'; };
Money.prototype.toISOString = function () { return 'Money.toISOString'; };
const m: any = new (Money as any)(1);
t('function-ctor m.toFixed(2) ', () => m.toFixed(2));
t('function-ctor m.getTime() ', () => m.getTime());
t('function-ctor m.toISOString()', () => m.toISOString());
class Clock {
getTime() { return 'Clock.getTime'; }
toFixed(d: number) { return 'Clock.toFixed'; }
setHours(h: number) { return 'Clock.setHours'; }
}
const c = new Clock();
t('class c.getTime() ', () => c.getTime());
t('class c.toFixed(2) ', () => c.toFixed(2));
t('class c.setHours(1) ', () => c.setHours(1));
const lit = { getFullYear() { return 'lit.getFullYear'; }, toPrecision(p: number) { return 'lit.toPrecision'; } };
t('object literal getFullYear() ', () => lit.getFullYear());
t('object literal toPrecision(3)', () => lit.toPrecision(3));node main.ts
perry compile main.ts -o out && ./outExpected (Node 26.5.1)
js this.toISOString() in toJSON Dayjs.toISOString(0)
function-ctor m.toFixed(2) Money.toFixed
function-ctor m.getTime() Money.getTime
function-ctor m.toISOString() Money.toISOString
class c.getTime() Clock.getTime
class c.toFixed(2) Clock.toFixed
class c.setHours(1) Clock.setHours
object literal getFullYear() lit.getFullYear
object literal toPrecision(3) lit.toPrecision
Actual (Perry)
Same output with and without PERRY_NO_AUTO_OPTIMIZE=1:
js this.toISOString() in toJSON THREW RangeError: Invalid time value
function-ctor m.toFixed(2) NaN
function-ctor m.getTime() [object Object]
function-ctor m.toISOString() THREW RangeError: Invalid time value
class c.getTime() [object Object]
class c.toFixed(2) NaN
class c.setHours(1) NaN
object literal getFullYear() NaN
object literal toPrecision(3) NaN
A zero-argument call of a user method named endsWith, includes or startsWith does not even compile. endswith.js
imported from main.ts:
function Path() {}
Path.prototype.endsWith = function () { return 'Path.endsWith'; };
console.log(new Path().endsWith());Error compiling module 'endswith.js' (...) with --backend llvm: lowering entry of module 'endswith.js': lowering init statements of non-entry module 'endswith.js': perry-codegen: String.endsWith expects 1 or 2 args, got 0
(Node prints Path.endsWith.)
Impact
- dayjs 1.11.21:
d.toISOString(),d.toJSON()(esm/index.js:496,return this.isValid() ? this.toISOString() : null) throwRangeError: Invalid time value, soJSON.stringify(dayjs())fails. - moment 2.30.1: same path (
moment.js:4329toJSON), plus thetoISOStringcall on a moment instance. - decimal.js 10.6.0 (
P.toFixed = function (dp, rm),decimal.js:2025) and bignumber.js 11.1.5 (P.toFixed,dist/bignumber.js:2673):toFixed/toExponential/toPrecisionreturn"NaN". That also breaks every money-formatting chain. - Any library class exposing
getTime(),setTime(),toFixed()ortoSorted(), e.g. timers, clocks, fixed-point and vector types. The class form is affected too, not only ES5 prototypes.
Notes
I swept all 185 identifier-named methods of the Date/Number/String/Array/RegExp/Map/Set/Promise/Object/Function/
Boolean/Symbol/Error/ArrayBuffer/TypedArray/WeakMap/BigInt/DataView prototypes. Each was defined as a user method
returning a sentinel and called in these forms: new F().m(), new F().m(2), this.m() inside another prototype
method, var P = F.prototype; P.m = …, F.prototype = { m }, Object.create({ m }), and class K { m() {} } (JS
module). Methods that never run:
| names | receivers affected |
|---|---|
getDate getDay getFullYear getHours getMilliseconds getMinutes getMonth getSeconds getTime getTimezoneOffset getUTCDate getUTCDay getUTCFullYear getUTCHours getUTCMilliseconds getUTCMinutes getUTCMonth getUTCSeconds and every set* counterpart (setDate … setUTCSeconds, setTime) |
all forms, including class instances and object literals |
toFixed toPrecision toExponential |
all forms |
toSorted toReversed |
all forms |
reduceRight hasOwnProperty propertyIsEnumerable (called with an argument) |
all forms, including class instances |
flatMap toSpliced (called with an argument) |
Object.create({ m }) receivers |
toISOString toDateString toTimeString toUTCString toGMTString, and zero-arg toLocaleString toLocaleDateString toLocaleTimeString |
this.m() inside a prototype method, and any-typed receivers in TS. A plain local in a JS module and class instances escape. |
endsWith includes startsWith with zero args |
compile error (above), on non-class receivers |
All String-named methods other than those three compile and run correctly. So do Map/Set/Promise/RegExp-named methods.
Suspected locations (inferred from reading the code, not bisected):
crates/perry-hir/src/lower/expr_call/url_date_instance.rs:149:receiver_may_be_dateis true unless the static
receiver class is one of URL/Object/Buffer/BlockList/SocketAddress/Uint8Array/Uint8ClampedArray/Array. Unknown
receivers and user classes fall into the Date arms ("getTime" => Expr::DateGetTime, line 231; setters, line 370).
OnlytoJSONis gated on a proven Date receiver.crates/perry-codegen/src/lower_call/property_get/number_string.rs:30/46/66:toFixed/toPrecision/toExponential
lower tojs_number_to_*for any receiver that is not provably a string or array ("tests often call it on Any locals").crates/perry-codegen/src/lower_string_method.rs:716: the aritybail!fires before the receiver is known to be a
string.
Related closed issues for the same name-collision class: #7673 (zod .trim() → String.prototype.trim), #5271 (joi
trim), #6065 (charAt/charCodeAt/codePointAt). Sibling audit bug: #10477 (decimal.js/bignumber.js instanceof).
Activity
docs(changelog): cover #10476's builtin-named method receiver guard
proggeramlug commentedon Sep 18, 2026
Fix is up as PR #10591. It lands with a known perf cost on the guarded path (3–4x Node on dayjs/decimal-shaped receivers), tracked separately as #10594 — the pre-fix baseline crashed or returned wrong answers on those shapes, so the old speed was not a valid comparator.
fix(codegen): guard builtin-named methods on the receiver's runtime kind
docs(changelog): cover #10476's builtin-named method receiver guard
test(parity): an own property must beat a proven-kind builtin (#10943 …
fix(codegen): an own property beats a proven-kind builtin (#10943 lay…
test(parity): an own property must beat a proven-kind builtin (#10943 …
fix(codegen): an own property beats a proven-kind builtin (#10943 lay…
test(parity): an own property must beat a proven-kind builtin (#10943 …
fix(codegen): an own property beats a proven-kind builtin (#10943 lay…
fix(codegen): own method beats a Date/Array builtin on unproven recei…
fix(codegen): own method beats a Date/Array builtin on unproven recei…