Found while verifying a CodeRabbit finding on PR #10592 (class_ref_id validation for the
instanceof dynamic-RHS classification, crates/perry-runtime/src/object/instanceof.rs). That fix
is correct and landed, but exposed a deeper issue this ticket is for.
Background
Perry's NaN-boxing reserves the tag band 0x7FFE_xxxx_xxxx_xxxx (top 16 bits) for INT32-tagged
class references — see class_constructor_ref_value/class_ref_id in
crates/perry-runtime/src/object/native_module/class_ref_values.rs. A JS-visible number can carry
the exact same bit pattern: any signaling-NaN payload is a legal IEEE-754 double, and user code can
construct one directly via DataView.
Reproduction (direct, no array/loop involved)
const dv = new DataView(new ArrayBuffer(8));
dv.setUint32(0, 0x7ffe0000, false);
dv.setUint32(4, 5, false);
const crafted: any = dv.getFloat64(0, false);
console.log(typeof crafted, Number.isNaN(crafted));
Expected (Node 26.5.1)
Actual (Perry, this build)
typeof is correct, but Number.isNaN is wrong — Perry does not recognize this exact bit pattern as
NaN. This is consistent with the tag band being reserved for an internal representation (class refs)
rather than treated as a generic double.
A second, harder-to-pin-down symptom: context-dependent instanceof misclassification
With the class_ref_id validation fix from #10592 applied, the same crafted value used directly as an
instanceof RHS correctly throws (Right-hand side of 'instanceof' is not an object), matching Node.
But inside test-files/test_gap_10479_instanceof_value_kinds.ts's full badRhs loop (a [string, any][] array literal with 7 entries, this crafted value as the 7th, iterated with for...of over two
nested loops), the SAME construction misbehaves:
sso3 instanceof craftedNaN: true (Node/expected: throws TypeError)
newA instanceof craftedNaN: false (Node/expected: throws TypeError)
null instanceof craftedNaN: false (Node/expected: throws TypeError)
This does not reproduce in a minimal 2-entry version of the same array/loop shape ([["{}", {}], ["craftedNaN", crafted]] misbehaves correctly / matches Node) — only the full ~200-line test file
context triggers it. That rules out a pure "array storage corrupts the NaN payload" theory (a minimal
array-storage repro round-trips the value fine) and points at something in the broader file — likely a
whole-program codegen specialization or inlining decision that changes how the dynamic
instanceof/class_ref_id classification resolves R once enough distinct call shapes of x instanceof R exist in one compilation unit. Not root-caused further; flagging with the two data points
above (isolated 2-entry case: correct; full-file 7-entry case: wrong) for whoever picks this up.
Suspected area
crates/perry-runtime/src/object/instanceof.rs (js_instanceof_dynamic's RHS classification) and/or
perry-codegen's handling of dynamic instanceof call sites when a function/module has many distinct
shapes of it — possibly PERRY_NO_AUTO_OPTIMIZE-sensitive (not checked either way this session).
Also worth checking whether Number.isNaN/general double round-tripping through DataView has a
broader gap for any payload in the 0x7FF8-0x7FFF band, independent of instanceof.
Impact
Low in practice — requires deliberately constructing a NaN payload via DataView to overlap Perry's
internal tag space, which is not something ordinary JS code does. Not currently known to break a
specific npm package.
Found while verifying a CodeRabbit finding on PR #10592 (
class_ref_idvalidation for theinstanceofdynamic-RHS classification,crates/perry-runtime/src/object/instanceof.rs). That fixis correct and landed, but exposed a deeper issue this ticket is for.
Background
Perry's NaN-boxing reserves the tag band
0x7FFE_xxxx_xxxx_xxxx(top 16 bits) for INT32-taggedclass references — see
class_constructor_ref_value/class_ref_idincrates/perry-runtime/src/object/native_module/class_ref_values.rs. A JS-visiblenumbercan carrythe exact same bit pattern: any signaling-NaN payload is a legal IEEE-754 double, and user code can
construct one directly via
DataView.Reproduction (direct, no array/loop involved)
Expected (Node 26.5.1)
Actual (Perry, this build)
typeofis correct, butNumber.isNaNis wrong — Perry does not recognize this exact bit pattern asNaN. This is consistent with the tag band being reserved for an internal representation (class refs)
rather than treated as a generic double.
A second, harder-to-pin-down symptom: context-dependent
instanceofmisclassificationWith the
class_ref_idvalidation fix from #10592 applied, the same crafted value used directly as aninstanceofRHS correctly throws (Right-hand side of 'instanceof' is not an object), matching Node.But inside
test-files/test_gap_10479_instanceof_value_kinds.ts's fullbadRhsloop (a[string, any][]array literal with 7 entries, this crafted value as the 7th, iterated withfor...ofover twonested loops), the SAME construction misbehaves:
This does not reproduce in a minimal 2-entry version of the same array/loop shape (
[["{}", {}], ["craftedNaN", crafted]]misbehaves correctly / matches Node) — only the full ~200-line test filecontext triggers it. That rules out a pure "array storage corrupts the NaN payload" theory (a minimal
array-storage repro round-trips the value fine) and points at something in the broader file — likely a
whole-program codegen specialization or inlining decision that changes how the dynamic
instanceof/class_ref_idclassification resolvesRonce enough distinct call shapes ofx instanceof Rexist in one compilation unit. Not root-caused further; flagging with the two data pointsabove (isolated 2-entry case: correct; full-file 7-entry case: wrong) for whoever picks this up.
Suspected area
crates/perry-runtime/src/object/instanceof.rs(js_instanceof_dynamic's RHS classification) and/orperry-codegen's handling of dynamicinstanceofcall sites when a function/module has many distinctshapes of it — possibly
PERRY_NO_AUTO_OPTIMIZE-sensitive (not checked either way this session).Also worth checking whether
Number.isNaN/general double round-tripping throughDataViewhas abroader gap for any payload in the
0x7FF8-0x7FFFband, independent ofinstanceof.Impact
Low in practice — requires deliberately constructing a NaN payload via
DataViewto overlap Perry'sinternal tag space, which is not something ordinary JS code does. Not currently known to break a
specific npm package.