Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/runtime-exact-int32-predicate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix: every "is this an int32 Number?" test in the runtime, the generated code and perry-ffi now uses one exact predicate (tag `0x7FFE` with bits 32..47 clear: `value::is_int32_bits`, emitted by `codegen::emit_int32_bits_test`). About 30 sites tested only the top 16 bits, so they read a `C.prototype` reference word (and any other `0x7FFE` word with high payload bits) as an int32 Number whose value is its class id. The affected paths were dynamic and keyed array indexing, typed-feedback and polymorphic index reads, Buffer and typed-array indexing, truthiness (a prototype reference of class id 0 read as falsy), JSON stringification, Date number conversion, the barrier's raw-slot canonicalization, the contract and check-mode kind classifiers, the GC census, and the generated strict and relational comparisons against a Number, the compare chains and the dynamic index-key normalizer. A non-Number `0x7FFE` word is now an object at each of these sites.
8 changes: 4 additions & 4 deletions crates/perry-codegen/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,8 @@ mod opts;
mod program_facts_view;
pub use program_facts_view::{
ElisionFacts, FactsBody, FactsClass, FactsElementRead, FactsField, FactsFieldReadCall,
FactsIndexedOwner,
FactsNumericExpr, FactsNumericFunction, FactsRecv, FactsRep, ProgramFactsView,
FactsIndexedOwner, FactsNumericExpr, FactsNumericFunction, FactsRecv, FactsRep,
ProgramFactsView,
};
#[cfg(test)]
mod ordinary_param_guard_tests;
Expand Down Expand Up @@ -360,8 +360,8 @@ mod typed_abi;
mod typed_abi_opt_report;
mod typed_entry;
pub(crate) use typed_entry::{
emit_int32_box_test, emit_ordered_number_test, emit_plain_number_bits_test,
emit_plain_number_test, typed_entry_arg_guard,
emit_int32_bits_test, emit_int32_box_test, emit_ordered_number_test,
emit_plain_number_bits_test, emit_plain_number_test, typed_entry_arg_guard,
};
#[cfg(test)]
mod unknown_func_tests;
Expand Down
16 changes: 4 additions & 12 deletions crates/perry-codegen/src/codegen/typed_abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,7 @@ pub(crate) fn emit_typed_i32_guard_and_raw(
use crate::types::{I1, I32, I64};

let bits = blk.bitcast_double_to_i64(value);
let int32_identity_mask = crate::nanbox::i64_literal(!crate::nanbox::INT32_MASK);
let tagged_identity = blk.and(I64, &bits, &int32_identity_mask);
let is_tagged = blk.icmp_eq(I64, &tagged_identity, crate::nanbox::INT32_TAG_I64);
let is_tagged = super::emit_int32_bits_test(blk, &bits);

let (plain_ok, plain_raw) = emit_typed_i32_plain_guard_and_raw(blk, value, &bits);
let admitted = blk.or(I1, &is_tagged, &plain_ok);
Expand All @@ -125,9 +123,7 @@ pub(crate) fn emit_typed_i32_raw_assuming_guarded(
use crate::types::{DOUBLE, I1, I32, I64};

let bits = blk.bitcast_double_to_i64(value);
let int32_identity_mask = crate::nanbox::i64_literal(!crate::nanbox::INT32_MASK);
let tagged_identity = blk.and(I64, &bits, &int32_identity_mask);
let is_tagged = blk.icmp_eq(I64, &tagged_identity, crate::nanbox::INT32_TAG_I64);
let is_tagged = super::emit_int32_bits_test(blk, &bits);
let safe_plain = blk.select(I1, &is_tagged, DOUBLE, "0.0", value);
let plain_raw = blk.fptosi(DOUBLE, &safe_plain, I32);
let tagged_raw = blk.trunc(I64, &bits, I32);
Expand Down Expand Up @@ -292,9 +288,7 @@ pub(crate) fn emit_typed_f64_guard(blk: &mut crate::block::LlBlock, arg: &str) -
&crate::nanbox::i64_literal(crate::nanbox::SHORT_STRING_TAG),
);
// JSValue::is_int32: the INT32 tag with any payload.
let int32_identity_mask = crate::nanbox::i64_literal(!crate::nanbox::INT32_MASK);
let tagged_identity = blk.and(I64, &bits, &int32_identity_mask);
let is_int32 = blk.icmp_eq(I64, &tagged_identity, crate::nanbox::INT32_TAG_I64);
let is_int32 = super::emit_int32_bits_test(blk, &bits);
blk.or(I1, &is_plain_number, &is_int32)
}

Expand All @@ -305,9 +299,7 @@ pub(crate) fn emit_typed_f64_guard(blk: &mut crate::block::LlBlock, arg: &str) -
pub(crate) fn emit_typed_f64_to_raw_guarded(blk: &mut crate::block::LlBlock, arg: &str) -> String {
use crate::types::{DOUBLE, I1, I32, I64};
let bits = blk.bitcast_double_to_i64(arg);
let int32_identity_mask = crate::nanbox::i64_literal(!crate::nanbox::INT32_MASK);
let tagged_identity = blk.and(I64, &bits, &int32_identity_mask);
let is_int32 = blk.icmp_eq(I64, &tagged_identity, crate::nanbox::INT32_TAG_I64);
let is_int32 = super::emit_int32_bits_test(blk, &bits);
let low = blk.trunc(I64, &bits, I32);
let converted = blk.sitofp(I32, &low, DOUBLE);
blk.select(I1, &is_int32, DOUBLE, &converted, arg)
Expand Down
9 changes: 8 additions & 1 deletion crates/perry-codegen/src/codegen/typed_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,15 @@ pub(crate) fn emit_ordered_number_test(blk: &mut LlBlock, value: &str) -> String
/// `JSValue::is_int32` — the INT32 tag with any 32-bit payload.
pub(crate) fn emit_int32_box_test(blk: &mut LlBlock, value: &str) -> String {
let bits = blk.bitcast_double_to_i64(value);
emit_int32_bits_test(blk, &bits)
}

/// THE emitted int32-Number test on raw bits (`perry_runtime::value::
/// is_int32_bits`): tag `0x7FFE` with bits 32..47 clear. A top-16-only test
/// reads a `C.prototype` ref or an internal marker as an int32 Number.
pub(crate) fn emit_int32_bits_test(blk: &mut LlBlock, bits: &str) -> String {
let identity_mask = crate::nanbox::i64_literal(!crate::nanbox::INT32_MASK);
let identity = blk.and(I64, &bits, &identity_mask);
let identity = blk.and(I64, bits, &identity_mask);
blk.icmp_eq(I64, &identity, crate::nanbox::INT32_TAG_I64)
}

Expand Down
23 changes: 5 additions & 18 deletions crates/perry-codegen/src/expr/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -766,12 +766,8 @@ fn lower_strict_eq_inline_any(
.block()
.icmp_eq(I64, &r_tag, crate::nanbox::SHORT_STRING_TAG_TOP16_I64);
let both_sso = ctx.block().and(I1, &l_sso, &r_sso);
let l_i32 = ctx
.block()
.icmp_eq(I64, &l_tag, crate::nanbox::INT32_TAG_TOP16_I64);
let r_i32 = ctx
.block()
.icmp_eq(I64, &r_tag, crate::nanbox::INT32_TAG_TOP16_I64);
let l_i32 = crate::codegen::emit_int32_bits_test(ctx.block(), &l_bits);
let r_i32 = crate::codegen::emit_int32_bits_test(ctx.block(), &r_bits);
let both_i32 = ctx.block().and(I1, &l_i32, &r_i32);
let canonical = ctx.block().or(I1, &both_sso, &both_i32);
ctx.block().cond_br(&canonical, &false_l, &slow_l);
Expand Down Expand Up @@ -826,10 +822,7 @@ fn is_plain_number_or_undefined(ctx: &FnCtx<'_>, expr: &Expr) -> bool {
/// participate in the numeric comparison.
fn normalize_int32_immediate(ctx: &mut FnCtx<'_>, value: &str) -> String {
let bits = ctx.block().bitcast_double_to_i64(value);
let top16 = ctx.block().lshr(I64, &bits, "48");
let is_i32 = ctx
.block()
.icmp_eq(I64, &top16, crate::nanbox::INT32_TAG_TOP16_I64);
let is_i32 = crate::codegen::emit_int32_bits_test(ctx.block(), &bits);
let raw = ctx.block().trunc(I64, &bits, I32);
let decoded = ctx.block().sitofp(I32, &raw, DOUBLE);
ctx.block().select(I1, &is_i32, DOUBLE, &decoded, value)
Expand Down Expand Up @@ -881,10 +874,7 @@ fn lower_strict_eq_against_number(
let compact = if plain {
None
} else {
let tag = ctx.block().lshr(I64, &bits, "48");
let is_i32 = ctx
.block()
.icmp_eq(I64, &tag, crate::nanbox::INT32_TAG_TOP16_I64);
let is_i32 = crate::codegen::emit_int32_bits_test(ctx.block(), &bits);
let integer = ctx
.block()
.call(I32, "llvm.fptosi.sat.i32.f64", &[(DOUBLE, number)]);
Expand Down Expand Up @@ -946,13 +936,10 @@ fn lower_relational_against_number(
) -> String {
let dynamic = if dynamic_is_left { l } else { r };
let bits = ctx.block().bitcast_double_to_i64(dynamic);
let top16 = ctx.block().lshr(I64, &bits, "48");
// Use the same raw-Number admission as the fully dynamic comparison.
// Negative IEEE values and canonical NaNs precede the positive tag band.
let is_plain_number = emit_is_unboxed_number(ctx, &bits);
let is_i32 = ctx
.block()
.icmp_eq(I64, &top16, crate::nanbox::INT32_TAG_TOP16_I64);
let is_i32 = crate::codegen::emit_int32_bits_test(ctx.block(), &bits);
// undefined/null/false/true are four consecutive immediate values.
// A range decides admission without separately classifying each value.
debug_assert_eq!(crate::nanbox::TAG_UNDEFINED + 3, crate::nanbox::TAG_TRUE);
Expand Down
5 changes: 1 addition & 4 deletions crates/perry-codegen/src/expr/compare_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,10 +221,7 @@ pub(crate) fn try_lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<Option<Strin
continue;
}
let bits = ctx.block().bitcast_double_to_i64(&boxed);
let top = ctx.block().lshr(I64, &bits, "48");
let int = ctx
.block()
.icmp_eq(I64, &top, crate::nanbox::INT32_TAG_TOP16_I64);
let int = crate::codegen::emit_int32_bits_test(ctx.block(), &bits);
if !strict_numeric_equality {
// The sign-closed plain-number test: negative IEEE values and
// canonical NaNs pass, every tag (HANDLE_TAG included) fails.
Expand Down
10 changes: 8 additions & 2 deletions crates/perry-codegen/src/expr/compare_chain_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ fn forty_dynamic_comparisons_classify_once_and_share_the_slow_call() {
let ir = ir(chain(40), vec![]);
assert_eq!(ir.matches("call double @js_rel_ge(").count(), 1, "{ir}");
assert_eq!(ir.matches("sitofp i32").count(), 1, "{ir}");
assert_eq!(ir.matches("lshr i64").count(), 1, "{ir}");
// The one exact int32 test (`emit_int32_bits_test`), shared by the graph.
assert_eq!(ir.matches(crate::nanbox::INT32_TAG_I64).count(), 1, "{ir}");
assert!(!ir.contains("lshr i64"), "{ir}");
assert!(ir.contains("phi i1"), "{ir}");
assert!(
!ir.contains("phi double") || ir.matches("phi double").count() <= 2,
Expand Down Expand Up @@ -170,7 +172,11 @@ fn mixed_graph_does_not_decode_a_constructively_numeric_local() {
};
let good = ir(expr.clone(), vec![local.clone()]);
assert_eq!(good.matches("sitofp i32").count(), 1, "{good}");
assert_eq!(good.matches("lshr i64").count(), 1, "{good}");
assert_eq!(
good.matches(crate::nanbox::INT32_TAG_I64).count(),
1,
"{good}"
);
let dynamic = ir(
expr,
vec![
Expand Down
13 changes: 11 additions & 2 deletions crates/perry-codegen/src/expr/compare_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -707,9 +707,18 @@ fn dynamic_strict_eq_against_number_compares_both_encodings_without_js_eq() {
Expr::LocalGet(X),
Expr::Number(7.0),
);
// The exact int32 test (`emit_int32_bits_test`): the identity mask
// (bits 32..63) compared with the whole INT32 tag word. A test of the
// top 16 bits alone (`lshr 48`, `icmp eq 32766`) reads a `C.prototype`
// ref or an internal marker as an int32 Number.
let mask = crate::nanbox::i64_literal(!crate::nanbox::INT32_MASK);
assert!(
ir.contains(crate::nanbox::INT32_TAG_TOP16_I64),
"the compact-INT32 normalization guard is absent:\n{ir}"
ir.contains(&mask) && ir.contains(crate::nanbox::INT32_TAG_I64),
"the compact-INT32 normalization guard is absent or inexact:\n{ir}"
);
assert!(
!ir.contains(&format!(", {}", crate::nanbox::INT32_TAG_TOP16_I64)),
"an int32 test compares only the top 16 bits:\n{ir}"
);
assert!(
ir.contains("fcmp oeq double"),
Expand Down
3 changes: 1 addition & 2 deletions crates/perry-codegen/src/expr/index_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,7 @@ fn publish(ctx: &mut FnCtx<'_>) {
f.no_inline = true;
let b = f.create_block("entry");
let bits = b.bitcast_double_to_i64("%key");
let tag = b.lshr(I64, &bits, "48");
let boxed = b.icmp_eq(I64, &tag, crate::nanbox::INT32_TAG_TOP16_I64);
let boxed = crate::codegen::emit_int32_bits_test(b, &bits);
b.cond_br(&boxed, "boxed.1", "raw.2");

let b = f.create_block("boxed");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ fn emit_receiver_kind_branch(
let band_offset = blk.sub(I64, &tag, crate::nanbox::SHORT_STRING_TAG_TOP16_I64);
let in_tag_band = blk.icmp_ult(I64, &band_offset, "7");
let is_double = blk.xor(I1, &in_tag_band, "true");
let is_int32 = blk.icmp_eq(I64, &tag, crate::nanbox::INT32_TAG_TOP16_I64);
let is_int32 = crate::codegen::emit_int32_bits_test(blk, &bits);
let is_number = blk.or(I1, &is_double, &is_int32);
let is_number = blk.and(I1, &is_number, pristine);
blk.cond_br(&is_number, builtin_label, generic_label);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-codegen/src/stmt/byte_result_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ fn admitted_general_byte_result_does_not_use_a_pointer_slot() {
"Number-or-undefined was put in a GC pointer slot:\n{result}"
);
assert_eq!(
result.matches("32766").count(),
result.matches(crate::nanbox::INT32_TAG_I64).count(),
1,
"only the constant-side Number normalizer needs a compact tag check:\n{result}"
);
Expand Down
11 changes: 1 addition & 10 deletions crates/perry-codegen/src/stmt/switch_stmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,16 +458,7 @@ struct NumericView {
/// strictly equal a number.
fn emit_canonical_numeric_view(ctx: &mut FnCtx<'_>, dv: &str) -> NumericView {
let bits = ctx.block().bitcast_double_to_i64(dv);
let int32_band = ctx.block().and(
I64,
&bits,
&crate::nanbox::i64_literal(!crate::nanbox::INT32_MASK),
);
let is_int32 = ctx.block().icmp_eq(
I64,
&int32_band,
&crate::nanbox::i64_literal(crate::nanbox::INT32_TAG),
);
let is_int32 = crate::codegen::emit_int32_bits_test(ctx.block(), &bits);
let lo32 = ctx.block().trunc(I64, &bits, I32);
let int32_as_double = ctx.block().sitofp(I32, &lo32, DOUBLE);
let canonical = ctx
Expand Down
11 changes: 9 additions & 2 deletions crates/perry-ffi/src/jsvalue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,9 @@ impl JsValue {
/// True if the value is a 32-bit integer.
#[inline]
pub const fn is_int32(self) -> bool {
(self.0 & TAG_MASK) == INT32_TAG
// Bits 32..47 clear as well: other `0x7FFE` words (a `C.prototype`
// ref, internal markers) are not Numbers.
(self.0 & !0x0000_0000_FFFF_FFFF) == INT32_TAG
}

/// True if the value is a number (real f64 OR int32). Returns
Expand Down Expand Up @@ -662,7 +664,12 @@ mod tests {
let v = JsValue::from_int32(n);
// One Number encoding: the raw f64 bits, never an INT32 word.
assert_eq!(v.bits(), (n as f64).to_bits(), "{n}");
assert!(!v.is_int32(), "{} must not be an INT32 word, got {:?}", n, v);
assert!(
!v.is_int32(),
"{} must not be an INT32 word, got {:?}",
n,
v
);
assert_eq!(v.to_int32(), n);
assert!(v.is_number());
assert_eq!(v.to_number(), n as f64);
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/array/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ pub(super) fn to_object(recv: f64) -> f64 {
// Numbers: real f64 values (`is_number`) and INT32-tagged small integers
// (0x7FFE), which `is_number` excludes because the tag sits in the
// string/special band.
if top16(b) == 0x7FFE || JSValue::from_bits(b).is_number() {
if crate::value::is_int32_bits(b) || JSValue::from_bits(b).is_number() {
return crate::builtins::js_boxed_number_new(recv);
}
// BigInt boxes into a `BigInt` wrapper object (`ToObject`): no indexed
Expand Down
6 changes: 3 additions & 3 deletions crates/perry-runtime/src/array/indexing_keyed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ pub extern "C" fn js_array_get_index_or_string(arr: *const ArrayHeader, idx: f64
return array_get_property_by_key(arr, key);
}

let numeric = if (bits & crate::value::TAG_MASK) == crate::value::INT32_TAG {
let numeric = if crate::value::is_int32_bits(bits) {
Some(crate::value::JSValue::from_bits(bits).as_int32() as f64)
} else if !(0x7FF8..=0x7FFF).contains(&top16) {
Some(idx)
Expand Down Expand Up @@ -278,7 +278,7 @@ pub extern "C" fn js_array_set_index_or_string(
// Treat numeric keys according to the array-index boundary. Only
// integers in 0..2^32-2 extend element storage; 2^32-1 and larger are
// ordinary string properties.
let numeric = if (bits & crate::value::TAG_MASK) == crate::value::INT32_TAG {
let numeric = if crate::value::is_int32_bits(bits) {
Some(crate::value::JSValue::from_bits(bits).as_int32() as f64)
} else if !(0x7FF8..=0x7FFF).contains(&top16) {
Some(idx)
Expand Down Expand Up @@ -449,7 +449,7 @@ fn canonical_index_of_set_key(idx: f64) -> Option<u32> {
return crate::object::canonical_array_index(name);
}
// Numeric key.
let n = if (bits & crate::value::TAG_MASK) == crate::value::INT32_TAG {
let n = if crate::value::is_int32_bits(bits) {
crate::value::JSValue::from_bits(bits).as_int32() as f64
} else if !(0x7FF8..=0x7FFF).contains(&top16) {
idx
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/array/indexing_read_cold.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ unsafe fn read_cold_dispatch(
/// and arrays with named storage. Sparse own values precede the prototype.
#[inline(always)]
unsafe fn plain_index_get(receiver: f64, key: f64) -> Option<f64> {
let n = if key.to_bits() >> 48 == 0x7FFE {
let n = if crate::value::is_int32_bits(key.to_bits()) {
key.to_bits() as i32 as f64
} else {
key
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-runtime/src/buffer/cmp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ fn buffer_search_needle_with_encoding(
};
}
}
let byte_val = if top16 == 0x7FFE {
let byte_val = if crate::value::is_int32_bits(needle_bits) {
(needle_bits as u32) & 0xFF
} else if top16 < 0x7FF8 || (top16 == 0x7FF8 && needle_bits == 0x7FF8_0000_0000_0000) {
((needle as i64) & 0xFF) as u32
Expand Down Expand Up @@ -301,7 +301,7 @@ pub extern "C" fn js_buffer_index_of_enc(
}
}
// Numeric byte needle — INT32_TAG or plain double
let byte_val = if top16 == 0x7FFE {
let byte_val = if crate::value::is_int32_bits(needle_bits) {
// INT32_TAG: lower 32 bits are an i32
(needle_bits as u32) & 0xFF
} else if top16 < 0x7FF8 || (top16 == 0x7FF8 && needle_bits == 0x7FF8_0000_0000_0000) {
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/byte_cell_support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ pub(crate) fn jsvalue_to_f64(v: f64) -> f64 {
crate::collection_iter::throw_type_error("Cannot convert a Symbol value to a number");
}
// INT32 tag
if top16 == 0x7FFE {
if crate::value::is_int32_bits(bits) {
let n = (bits & 0xFFFF_FFFF) as i32;
return n as f64;
}
Expand Down
3 changes: 1 addition & 2 deletions crates/perry-runtime/src/closure/dispatch/value_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,8 +407,7 @@ unsafe fn call_array_this(
// conversion at this bridge, and leave class references (the same tag)
// intact. Only a slice containing a numeric INT32 needs scratch storage.
let is_int = |raw: f64| {
raw.to_bits() & 0xFFFF_0000_0000_0000 == crate::value::INT32_TAG
&& crate::object::class_ref_id(raw).is_none()
crate::value::is_int32_bits(raw.to_bits()) && crate::object::class_ref_id(raw).is_none()
};
if !args.iter().copied().any(is_int) {
return dispatch_body_args(closure, info, this, args);
Expand Down
16 changes: 15 additions & 1 deletion crates/perry-runtime/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,9 @@ fn kind_of(v: f64) -> u16 {
}
JS_HANDLE_TAG => K_OBJ,
STRING_TAG | SHORT_STRING_TAG => K_STR,
INT32_TAG => K_NUM,
INT32_TAG if crate::value::is_int32_bits(bits) => K_NUM,
// A `C.prototype` ref or another non-Number `0x7FFE` word.
INT32_TAG => K_OBJ,
BIGINT_TAG => K_BIGINT,
TAG_MARKER => match bits & 0xFF {
2 => K_NULL,
Expand Down Expand Up @@ -749,4 +751,16 @@ mod tests {
assert_eq!(kind_of(f64::from_bits(STRING_TAG | 0x1000)), K_STR);
assert_eq!(kind_of(f64::from_bits(BIGINT_TAG | 0x1000)), K_BIGINT);
}
/// A `C.prototype` ref word (`CLASS_PROTOTYPE_REF_FLAG`, bit 32) for class
/// 5, and a `0x7FFE` word with high payload bits: neither is an int32
/// Number, and a top-16-only test reads both as one.
const PROTO_REF_5: u64 = 0x7FFE_0001_0000_0005;
const HIGH_BITS: u64 = 0x7FFE_00FF_0000_0001;

#[test]
fn a_non_number_0x7ffe_word_is_not_k_num() {
assert_eq!(kind_of(f64::from_bits(INT32_TAG | 3)), K_NUM);
assert_eq!(kind_of(f64::from_bits(PROTO_REF_5)), K_OBJ);
assert_eq!(kind_of(f64::from_bits(HIGH_BITS)), K_OBJ);
}
}
4 changes: 3 additions & 1 deletion crates/perry-runtime/src/date.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,10 +546,12 @@ fn jsvalue_to_number(v: f64) -> f64 {
_ => f64::NAN,
}
}
0x7FFE => {
0x7FFE if crate::value::is_int32_bits(bits) => {
// INT32
((bits & 0xFFFF_FFFF) as u32 as i32) as f64
}
// A non-Number `0x7FFE` word (a `C.prototype` ref) is an object.
0x7FFE => f64::NAN,
0x7FFD => {
// Pointer: a Date contributes its time value; any other object (and
// arrays) coerce via OrdinaryToPrimitive(number) → ToNumber.
Expand Down
Loading
Loading