fix(runtime): #748 follow-up — Invalid Date is now typeof "object" / instanceof Date#816
Merged
Conversation
…instanceof Date `new Date(NaN)` (and the zero-date branch of `@perryts/mysql`'s `MyDateTime.toDate()`) escaped the runtime as an untagged bare NaN — `typeof` reported `"number"`, `instanceof Date` returned `false`, and the string formatters cast NaN-as-i64 to 0 and emitted bogus `1970-01-01…` strings. That reproduced the v0.5.912 #748 symptom (route promise resolving to `0.0`) on shop-admin signup whenever any await chain returned an Invalid Date. Fix: a single canonical sentinel `DATE_NAN_BITS = 0x7FF8_0000_0000_0DA7` (quiet NaN in the 0x7FF8 space `JSValue::is_number` already treats as a plain number, so arithmetic and existing `is_nan()` getter guards keep working). Recognized by exact bit pattern globally — no thread-local registration step needed, so cross-thread Invalid Dates (mysql row decode off the socket thread) work for free. `is_registered_date_bits` short-circuits on the sentinel before consulting the existing HashSet; `js_date_new_from_timestamp` / `_from_value` route NaN through `date_or_invalid`; both `js_instanceof` arms (CLASS_ID_DATE, CLASS_ID_OBJECT) match the sentinel before their `!is_nan()` guard. The six string formatters early-return `"Invalid Date"` for NaN inputs. The finite-Date registry path is byte-identical to before — the reverted attempt in the issue comment regressed `Date.UTC(...)` valid dates by also touching finite registration; we deliberately don't. Closes #748 follow-up.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to v0.5.912's #748 condvar fix. The condvar wait is correct,
but a separate
Date-representation bug in the runtime kept theshop-admin signup symptom alive whenever an await chain produced an
Invalid Date — see the issue comment
for the full bisect.
What was wrong
Perry stores
Dateas a raw f64 timestamp with no NaN-box tag andconsults a thread-local
DATE_REGISTRY: HashSet<u64>fromtypeof/instanceof Date/ JSON.js_date_new_from_valueskipped registrationwhen
result.is_nan(), sonew Date(NaN)(andnew Date("nope"), andthe zero-date branch of
@perryts/mysql'sMyDateTime.toDate())escaped as a bare untagged NaN. Both
instanceofarms inobject.rsalso gated on
!is_nan() && is_finite()before consulting theregistry, so even a hypothetically-registered NaN couldn't match.
Net: ECMA-262 §21.4.1.1 says
new Date(NaN)is still a Date —typeof "object",instanceof Datetrue, time value NaN — and perry got allthree wrong. The six string formatters also lacked NaN guards and cast
NaN as i64 → 0, producing the1970-01-01T00:00:00.000Z<garbage>strings the issue reports.
The deterministic 3-line repro from the comment:
What this PR does
Single canonical Invalid-Date sentinel
DATE_NAN_BITS = 0x7FF8_0000_0000_0DA7— a quiet NaN in the 0x7FF8 spaceJSValue::is_numberalready treats as a plain number (not a NaN-box tag), so it flows
through arithmetic and every
if timestamp.is_nan() { return f64::NAN }getter exactly like a bare NaN. Recognition is by exact bit pattern,
globally, so the sentinel works across the socket-thread / main-thread
boundary without registration.
is_registered_date_bitsshort-circuitsbits == DATE_NAN_BITSbefore the existing HashSet lookup.
js_date_new_from_timestamp/js_date_new_from_valueroute NaNthrough
date_or_invalid→ the sentinel.js_instanceofarms (CLASS_ID_DATE,CLASS_ID_OBJECT) matchthe sentinel before their
!is_nan()guard.js_date_to_*_stringformatters early-return"Invalid Date"for NaN inputs.
typeofis already correct —js_value_typeof's f64 fallthroughcalls
is_registered_date_bits(bits)unconditionally, no NaN gate.The finite-Date registry path is byte-identical to before. The
reverted attempt described in the issue comment regressed
Date.UTC(…)valid dates by also touching finite registration; we deliberately do
not. Options 1 and 2-second-half from the comment (a dedicated NaN-box
tag for Date, or eliminating the value-keyed registry altogether) stay
out of scope — those address the broader
100 instanceof Datefalse-positive and cross-thread loss cases and need a workspace-wide
refactor.
Validation
test-files/test_issue_748_invalid_date.ts— compiled and run againstthe patched runtime:
Files:
crates/perry-runtime/src/date.rs,crates/perry-runtime/src/object.rs,plus version bump (
Cargo.toml,Cargo.lock,CLAUDE.md) and theCHANGELOG.mdentry. ~50 lines of net runtime code change.Closes the #748 follow-up.