diff --git a/changelog.d/runtime-exact-int32-predicate.md b/changelog.d/runtime-exact-int32-predicate.md new file mode 100644 index 00000000000..14c25b15ede --- /dev/null +++ b/changelog.d/runtime-exact-int32-predicate.md @@ -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. diff --git a/crates/perry-codegen/src/codegen/mod.rs b/crates/perry-codegen/src/codegen/mod.rs index 200943e6621..175137312d3 100644 --- a/crates/perry-codegen/src/codegen/mod.rs +++ b/crates/perry-codegen/src/codegen/mod.rs @@ -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; @@ -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; diff --git a/crates/perry-codegen/src/codegen/typed_abi.rs b/crates/perry-codegen/src/codegen/typed_abi.rs index 10cdf40ea0c..e8b9b14665a 100644 --- a/crates/perry-codegen/src/codegen/typed_abi.rs +++ b/crates/perry-codegen/src/codegen/typed_abi.rs @@ -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); @@ -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); @@ -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) } @@ -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) diff --git a/crates/perry-codegen/src/codegen/typed_entry.rs b/crates/perry-codegen/src/codegen/typed_entry.rs index 16f7b004046..17c0f000120 100644 --- a/crates/perry-codegen/src/codegen/typed_entry.rs +++ b/crates/perry-codegen/src/codegen/typed_entry.rs @@ -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) } diff --git a/crates/perry-codegen/src/expr/compare.rs b/crates/perry-codegen/src/expr/compare.rs index 7fd8fb1b554..77b6210a3d6 100644 --- a/crates/perry-codegen/src/expr/compare.rs +++ b/crates/perry-codegen/src/expr/compare.rs @@ -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); @@ -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) @@ -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)]); @@ -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); diff --git a/crates/perry-codegen/src/expr/compare_chain.rs b/crates/perry-codegen/src/expr/compare_chain.rs index 893022d9fb1..239d0a47444 100644 --- a/crates/perry-codegen/src/expr/compare_chain.rs +++ b/crates/perry-codegen/src/expr/compare_chain.rs @@ -221,10 +221,7 @@ pub(crate) fn try_lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result) { 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"); diff --git a/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard.rs b/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard.rs index 83525c2123a..31401fbce03 100644 --- a/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard.rs +++ b/crates/perry-codegen/src/lower_call/property_get/builtin_kind_guard.rs @@ -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); diff --git a/crates/perry-codegen/src/stmt/byte_result_tests.rs b/crates/perry-codegen/src/stmt/byte_result_tests.rs index 3dc452ba157..73210036756 100644 --- a/crates/perry-codegen/src/stmt/byte_result_tests.rs +++ b/crates/perry-codegen/src/stmt/byte_result_tests.rs @@ -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}" ); diff --git a/crates/perry-codegen/src/stmt/switch_stmt.rs b/crates/perry-codegen/src/stmt/switch_stmt.rs index 5848b6c68c6..74316a091a8 100644 --- a/crates/perry-codegen/src/stmt/switch_stmt.rs +++ b/crates/perry-codegen/src/stmt/switch_stmt.rs @@ -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 diff --git a/crates/perry-ffi/src/jsvalue.rs b/crates/perry-ffi/src/jsvalue.rs index 30617a87953..a11c202db27 100644 --- a/crates/perry-ffi/src/jsvalue.rs +++ b/crates/perry-ffi/src/jsvalue.rs @@ -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 @@ -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); diff --git a/crates/perry-runtime/src/array/generic.rs b/crates/perry-runtime/src/array/generic.rs index 478d02a25d8..41df14be50a 100644 --- a/crates/perry-runtime/src/array/generic.rs +++ b/crates/perry-runtime/src/array/generic.rs @@ -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 diff --git a/crates/perry-runtime/src/array/indexing_keyed.rs b/crates/perry-runtime/src/array/indexing_keyed.rs index 091f39c8461..67262653db0 100644 --- a/crates/perry-runtime/src/array/indexing_keyed.rs +++ b/crates/perry-runtime/src/array/indexing_keyed.rs @@ -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) @@ -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) @@ -449,7 +449,7 @@ fn canonical_index_of_set_key(idx: f64) -> Option { 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 diff --git a/crates/perry-runtime/src/array/indexing_read_cold.rs b/crates/perry-runtime/src/array/indexing_read_cold.rs index b1b1d93e1c3..d62e915ad37 100644 --- a/crates/perry-runtime/src/array/indexing_read_cold.rs +++ b/crates/perry-runtime/src/array/indexing_read_cold.rs @@ -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 { - 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 diff --git a/crates/perry-runtime/src/buffer/cmp.rs b/crates/perry-runtime/src/buffer/cmp.rs index a67a2da5c06..fefee80468b 100644 --- a/crates/perry-runtime/src/buffer/cmp.rs +++ b/crates/perry-runtime/src/buffer/cmp.rs @@ -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 @@ -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) { diff --git a/crates/perry-runtime/src/byte_cell_support/mod.rs b/crates/perry-runtime/src/byte_cell_support/mod.rs index 7823a0bb98e..d2c14a3cf8c 100644 --- a/crates/perry-runtime/src/byte_cell_support/mod.rs +++ b/crates/perry-runtime/src/byte_cell_support/mod.rs @@ -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; } diff --git a/crates/perry-runtime/src/closure/dispatch/value_call.rs b/crates/perry-runtime/src/closure/dispatch/value_call.rs index df3cbc445ad..6cb3f3e075a 100644 --- a/crates/perry-runtime/src/closure/dispatch/value_call.rs +++ b/crates/perry-runtime/src/closure/dispatch/value_call.rs @@ -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); diff --git a/crates/perry-runtime/src/contracts.rs b/crates/perry-runtime/src/contracts.rs index dbc37bcfa6b..54aa1c86b90 100644 --- a/crates/perry-runtime/src/contracts.rs +++ b/crates/perry-runtime/src/contracts.rs @@ -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, @@ -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); + } } diff --git a/crates/perry-runtime/src/date.rs b/crates/perry-runtime/src/date.rs index ae35b1d1fdf..cc685be40d0 100644 --- a/crates/perry-runtime/src/date.rs +++ b/crates/perry-runtime/src/date.rs @@ -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. diff --git a/crates/perry-runtime/src/date/tests.rs b/crates/perry-runtime/src/date/tests.rs index fef299c8abf..7f9e7e5b314 100644 --- a/crates/perry-runtime/src/date/tests.rs +++ b/crates/perry-runtime/src/date/tests.rs @@ -407,3 +407,19 @@ fn a_headerless_symbol_behind_a_date_cell_tag_is_not_a_date() { let date = alloc_date_cell(0.0); assert!(is_date_value(date)); } + +/// 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_converts_to_nan() { + assert_eq!( + jsvalue_to_number(f64::from_bits(0x7FFE_0000_0000_0005)), + 5.0 + ); + assert!(jsvalue_to_number(f64::from_bits(PROTO_REF_5)).is_nan()); + assert!(jsvalue_to_number(f64::from_bits(PROTO_REF_5)).to_bits() == f64::NAN.to_bits()); +} diff --git a/crates/perry-runtime/src/gc/barrier_store.rs b/crates/perry-runtime/src/gc/barrier_store.rs index 3320a746cd2..5604c426595 100644 --- a/crates/perry-runtime/src/gc/barrier_store.rs +++ b/crates/perry-runtime/src/gc/barrier_store.rs @@ -86,7 +86,7 @@ fn canonicalize_typed_slot_store_bits( slot_index: usize, value_bits: u64, ) -> u64 { - if value_bits & TAG_MASK != crate::value::INT32_TAG { + if !crate::value::is_int32_bits(value_bits) { return value_bits; } if !slot_holds_raw_f64(parent_user, slot_index) { diff --git a/crates/perry-runtime/src/gc/census.rs b/crates/perry-runtime/src/gc/census.rs index 5fbebeef12a..69c5dc7d95c 100644 --- a/crates/perry-runtime/src/gc/census.rs +++ b/crates/perry-runtime/src/gc/census.rs @@ -281,7 +281,9 @@ fn slot_kind(bits: u64) -> usize { POINTER_TAG => 0, STRING_TAG => 1, SHORT_STRING_TAG => 2, - INT32_TAG => 3, + INT32_TAG if crate::value::is_int32_bits(bits) => 3, + // A non-Number `0x7FFE` word (a `C.prototype` ref). + INT32_TAG => 11, BIGINT_TAG => 9, JS_HANDLE_TAG => 10, TAG_MARKER => match bits { @@ -644,6 +646,16 @@ pub(super) fn side_tables() -> Vec { rows } +#[cfg(test)] +mod int32_exact_tests { + #[test] + fn a_non_number_0x7ffe_word_is_not_the_int32_kind() { + assert_eq!(super::slot_kind(0x7FFE_0000_0000_0005), 3); + assert_ne!(super::slot_kind(0x7FFE_0001_0000_0005), 3); + assert_ne!(super::slot_kind(0x7FFE_00FF_0000_0001), 3); + } +} + #[cfg(test)] mod regex_census_tests { #[test] diff --git a/crates/perry-runtime/src/gc/census_field_repr.rs b/crates/perry-runtime/src/gc/census_field_repr.rs index e1704037d05..4247da24b96 100644 --- a/crates/perry-runtime/src/gc/census_field_repr.rs +++ b/crates/perry-runtime/src/gc/census_field_repr.rs @@ -41,7 +41,8 @@ const C_NUMBER: u8 = C_INT32 | C_DOUBLE | C_NAN_INF; pub(super) fn value_class(bits: u64) -> u8 { use crate::value::*; match bits & TAG_MASK { - INT32_TAG => C_INT32, + INT32_TAG if crate::value::is_int32_bits(bits) => C_INT32, + INT32_TAG => C_OTHER, POINTER_TAG | STRING_TAG | SHORT_STRING_TAG | BIGINT_TAG | JS_HANDLE_TAG | TAG_MARKER => { C_OTHER } @@ -234,4 +235,10 @@ mod tests { assert_eq!(s["f64_slots"], REP_SLOTS as u64); assert_eq!(s["f64_nan_inf"], 0); } + + #[test] + fn a_non_number_0x7ffe_word_is_not_c_int32() { + assert_eq!(value_class(0x7FFE_0001_0000_0005), C_OTHER); + assert_eq!(value_class(0x7FFE_00FF_0000_0001), C_OTHER); + } } diff --git a/crates/perry-runtime/src/json/stringify_flat.rs b/crates/perry-runtime/src/json/stringify_flat.rs index ab8902a8418..1b8907b3739 100644 --- a/crates/perry-runtime/src/json/stringify_flat.rs +++ b/crates/perry-runtime/src/json/stringify_flat.rs @@ -172,7 +172,7 @@ pub(super) unsafe fn scalar_piece(bits: u64) -> Option { match bits & crate::value::TAG_MASK { STRING_TAG | crate::value::SHORT_STRING_TAG => return string_piece(bits), POINTER_TAG | BIGINT_TAG | crate::value::HANDLE_TAG => return None, - INT32_TAG => { + INT32_TAG if crate::value::is_int32_bits(bits) => { let mut digits = itoa::Buffer::new(); return Some(Piece::inline( digits.format((bits & INT32_MASK) as u32 as i32).as_bytes(), diff --git a/crates/perry-runtime/src/json/stringify_scalars.rs b/crates/perry-runtime/src/json/stringify_scalars.rs index 311e7e67720..7953e534140 100644 --- a/crates/perry-runtime/src/json/stringify_scalars.rs +++ b/crates/perry-runtime/src/json/stringify_scalars.rs @@ -30,7 +30,7 @@ pub(crate) unsafe fn write_number(buf: &mut String, value: f64) { // the sqlite binding (and other int32-tagged numbers) funnel here; numeric // literals are stored as plain f64 doubles by codegen and never take this // branch. Mirrors the BigInt funnel above. - if (value.to_bits() & 0xFFFF_0000_0000_0000) == INT32_TAG { + if crate::value::is_int32_bits(value.to_bits()) { let n = (value.to_bits() & INT32_MASK) as u32 as i32; let mut itoa_buf = itoa::Buffer::new(); super::stringify_copy::push_str(buf, itoa_buf.format(n)); diff --git a/crates/perry-runtime/src/node_submodules/diagnostics_tail.rs b/crates/perry-runtime/src/node_submodules/diagnostics_tail.rs index 421a8d9fa44..0b683dc9dc5 100644 --- a/crates/perry-runtime/src/node_submodules/diagnostics_tail.rs +++ b/crates/perry-runtime/src/node_submodules/diagnostics_tail.rs @@ -74,7 +74,7 @@ fn store_handle(store: f64) -> Option { return Some(token); } let bits = store.to_bits(); - if bits & crate::value::TAG_MASK == crate::value::INT32_TAG { + if crate::value::is_int32_bits(bits) { return Some((bits & crate::value::INT32_MASK) as i32 as i64); } if bits & crate::value::TAG_MASK == crate::value::POINTER_TAG { diff --git a/crates/perry-runtime/src/object/buffer_dispatch.rs b/crates/perry-runtime/src/object/buffer_dispatch.rs index fede9eb3abb..a7b3fbca476 100644 --- a/crates/perry-runtime/src/object/buffer_dispatch.rs +++ b/crates/perry-runtime/src/object/buffer_dispatch.rs @@ -1148,7 +1148,7 @@ pub unsafe fn dispatch_buffer_method( .is_some_and(|idx| idx < crate::buffer::store::raw_length(buf_ptr as usize)) }) { own - } else if (key_bits >> 48) == 0x7FFE { + } else if crate::value::is_int32_bits(key_bits) { // int32 key let idx = (key_bits & 0xFFFF_FFFF) as i32; let buf_len = crate::buffer::store::raw_length(buf_ptr as usize) as i32; @@ -1186,7 +1186,7 @@ pub unsafe fn dispatch_buffer_method( .is_some_and(|idx| idx < crate::buffer::store::raw_length(buf_ptr as usize)) }) { own - } else if (key_bits >> 48) == 0x7FFE { + } else if crate::value::is_int32_bits(key_bits) { let idx = (key_bits & 0xFFFF_FFFF) as i32; let buf_len = crate::buffer::store::raw_length(buf_ptr as usize) as i32; idx >= 0 && idx < buf_len diff --git a/crates/perry-runtime/src/object/field_get_set/runtime_read_site.rs b/crates/perry-runtime/src/object/field_get_set/runtime_read_site.rs index a09bd8a1481..6f369c6e7da 100644 --- a/crates/perry-runtime/src/object/field_get_set/runtime_read_site.rs +++ b/crates/perry-runtime/src/object/field_get_set/runtime_read_site.rs @@ -235,7 +235,7 @@ pub(crate) unsafe fn get_atom(obj: *mut ObjectHeader, atom: *const crate::String #[inline] pub(crate) fn integer_key(key: f64) -> Option { let bits = key.to_bits(); - if bits & crate::value::TAG_MASK == crate::value::INT32_TAG { + if crate::value::is_int32_bits(bits) { return Some(i64::from(crate::value::JSValue::from_bits(bits).as_int32())); } (key.is_finite() && key.fract() == 0.0 && key.abs() <= 9_007_199_254_740_992.0) diff --git a/crates/perry-runtime/src/object/native_module_dispatch.rs b/crates/perry-runtime/src/object/native_module_dispatch.rs index cff8b915903..bf43a608f88 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch.rs @@ -140,7 +140,7 @@ macro_rules! nm_general_closures { let $i32_arg = |n: usize| -> i32 { let v = $arg(n); let bits = v.to_bits(); - if (bits >> 48) == 0x7FFE { + if crate::value::is_int32_bits(bits) { return (bits & 0xFFFF_FFFF) as u32 as i32; } if v.is_nan() || v.is_infinite() { diff --git a/crates/perry-runtime/src/object/object_ops/define_property.rs b/crates/perry-runtime/src/object/object_ops/define_property.rs index 6ccc84df186..2f476cc680a 100644 --- a/crates/perry-runtime/src/object/object_ops/define_property.rs +++ b/crates/perry-runtime/src/object/object_ops/define_property.rs @@ -323,8 +323,9 @@ pub(crate) unsafe fn apply_property_descriptor( // through array hole/OOB reads — flip the global flag. { let kb = key_value.to_bits(); - let is_numeric_key = - (kb >> 48) == 0x7FFE || crate::value::JSValue::from_bits(kb).is_number() || { + let is_numeric_key = crate::value::is_int32_bits(kb) + || crate::value::JSValue::from_bits(kb).is_number() + || { let sp = crate::value::js_get_string_pointer_unified(key_value) as *const crate::StringHeader; !sp.is_null() diff --git a/crates/perry-runtime/src/object/polymorphic_index.rs b/crates/perry-runtime/src/object/polymorphic_index.rs index 5d27d424fbc..2926311701c 100644 --- a/crates/perry-runtime/src/object/polymorphic_index.rs +++ b/crates/perry-runtime/src/object/polymorphic_index.rs @@ -96,7 +96,7 @@ fn integer_key_probe(raw: u64, idx: f64) -> Result { fn numeric_key_u32_index(value: f64) -> Option { let bits = value.to_bits(); - if (bits & crate::value::TAG_MASK) == crate::value::INT32_TAG { + if crate::value::is_int32_bits(bits) { let index = crate::value::JSValue::from_bits(bits).as_int32(); return (index >= 0).then_some(index as u32); } @@ -109,7 +109,7 @@ fn numeric_key_u32_index(value: f64) -> Option { fn numeric_key_i32_index(value: f64) -> Option { let bits = value.to_bits(); - if (bits & crate::value::TAG_MASK) == crate::value::INT32_TAG { + if crate::value::is_int32_bits(bits) { let index = crate::value::JSValue::from_bits(bits).as_int32(); return (index >= 0).then_some(index); } @@ -177,7 +177,8 @@ pub extern "C" fn js_object_get_index_polymorphic(obj_handle: i64, idx: f64) -> } // A `C.prototype` ref word; an INT32 number has no elements. 0x7FFE => { - if crate::object::class_prototype_ref_id(f64::from_bits(obj_handle as u64)).is_some() + if crate::object::class_prototype_ref_id(f64::from_bits(obj_handle as u64)) + .is_some() { return crate::value::js_dyn_index_get(f64::from_bits(obj_handle as u64), idx); } @@ -258,8 +259,7 @@ pub extern "C" fn js_object_get_index_polymorphic(obj_handle: i64, idx: f64) -> // own, then its [[Prototype]] chain, never the Buffer table. if crate::buffer::is_non_indexed_buffer_view(raw as usize) { if let Some(name) = buffer_key_name(idx) { - let key = - crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); + let key = crate::string::js_string_from_bytes(name.as_ptr(), name.len() as u32); let v = crate::object::view_get(raw as *const crate::object::ObjectHeader, key); return f64::from_bits(v.bits()); } @@ -605,3 +605,7 @@ fn buffer_key_name(idx: f64) -> Option { Some(String::from_utf8_lossy(bytes).into_owned()) } } + +#[cfg(test)] +#[path = "polymorphic_index_int32_exact_tests.rs"] +mod int32_exact_tests; diff --git a/crates/perry-runtime/src/object/polymorphic_index_int32_exact_tests.rs b/crates/perry-runtime/src/object/polymorphic_index_int32_exact_tests.rs new file mode 100644 index 00000000000..7f8b2d9a962 --- /dev/null +++ b/crates/perry-runtime/src/object/polymorphic_index_int32_exact_tests.rs @@ -0,0 +1,16 @@ +//! A non-Number `0x7FFE` word (a `C.prototype` ref) is no numeric key. + +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_no_numeric_key() { + for bits in [PROTO_REF_5, HIGH_BITS] { + assert_eq!(super::numeric_key_u32_index(f64::from_bits(bits)), None); + assert_eq!(super::numeric_key_i32_index(f64::from_bits(bits)), None); + } + assert_eq!( + super::numeric_key_u32_index(f64::from_bits(0x7FFE_0000_0000_0005)), + Some(5) + ); +} diff --git a/crates/perry-runtime/src/pty/mod.rs b/crates/perry-runtime/src/pty/mod.rs index 805a1cca7b2..1ef2f137302 100644 --- a/crates/perry-runtime/src/pty/mod.rs +++ b/crates/perry-runtime/src/pty/mod.rs @@ -285,7 +285,7 @@ mod platform_impl { /// Best-effort i32 from a NaN-boxed numeric argument. fn pty_arg_i32(v: f64) -> i32 { let bits = v.to_bits(); - if (bits >> 48) == 0x7FFE { + if crate::value::is_int32_bits(bits) { return (bits & 0xFFFF_FFFF) as u32 as i32; } if v.is_finite() { diff --git a/crates/perry-runtime/src/thread.rs b/crates/perry-runtime/src/thread.rs index d6b541aca08..b4ba51bf964 100644 --- a/crates/perry-runtime/src/thread.rs +++ b/crates/perry-runtime/src/thread.rs @@ -176,9 +176,13 @@ fn is_truthy_bits(bits: u64) -> bool { return true; } // INT32_TAG: check if value is 0 - if (bits & TAG_MASK) == INT32_TAG { + if crate::value::is_int32_bits(bits) { return (bits & INT32_MASK) != 0; } + // Any other `0x7FFE` word (a `C.prototype` ref) is an object: truthy. + if (bits & TAG_MASK) == INT32_TAG { + return true; + } // String: empty string is falsy (check length == 0) if (bits & TAG_MASK) == STRING_TAG { let ptr = (bits & POINTER_MASK) as *const crate::string::StringHeader; @@ -1470,3 +1474,7 @@ mod error_stack_clone_tests; #[cfg(test)] #[path = "thread/error_cause_clone_tests.rs"] mod error_cause_clone_tests; + +#[cfg(test)] +#[path = "thread_int32_exact_tests.rs"] +mod int32_exact_tests; diff --git a/crates/perry-runtime/src/thread_int32_exact_tests.rs b/crates/perry-runtime/src/thread_int32_exact_tests.rs new file mode 100644 index 00000000000..609986a6913 --- /dev/null +++ b/crates/perry-runtime/src/thread_int32_exact_tests.rs @@ -0,0 +1,10 @@ +//! Worker truthiness: a `C.prototype` ref word is an object (truthy), even +//! with class id 0 in its payload. + +#[test] +fn a_prototype_ref_word_is_truthy() { + assert!(super::is_truthy_bits(0x7FFE_0001_0000_0000)); + assert!(super::is_truthy_bits(0x7FFE_0001_0000_0005)); + assert!(!super::is_truthy_bits(0x7FFE_0000_0000_0000)); + assert!(super::is_truthy_bits(0x7FFE_0000_0000_0005)); +} diff --git a/crates/perry-runtime/src/typed_feedback.rs b/crates/perry-runtime/src/typed_feedback.rs index 55bd585e478..b710cf24ed2 100644 --- a/crates/perry-runtime/src/typed_feedback.rs +++ b/crates/perry-runtime/src/typed_feedback.rs @@ -491,7 +491,9 @@ fn stable_value_kind(bits: u64) -> u16 { BIGINT_TAG => STABLE_VALUE_BIGINT, JS_HANDLE_TAG => STABLE_VALUE_JS_HANDLE, SHORT_STRING_TAG => STABLE_VALUE_SHORT_STRING, - INT32_TAG => STABLE_VALUE_INT32, + INT32_TAG if crate::value::is_int32_bits(bits) => STABLE_VALUE_INT32, + // A non-Number `0x7FFE` word (a `C.prototype` ref): no stable kind. + INT32_TAG => STABLE_VALUE_POINTER, _ => STABLE_VALUE_NUMBER, } } @@ -1204,7 +1206,7 @@ fn is_numeric_value_bits(bits: u64) -> bool { fn finite_nonnegative_u32_index(index: f64) -> Option { let bits = index.to_bits(); - if (bits & TAG_MASK) == INT32_TAG { + if crate::value::is_int32_bits(bits) { let index = crate::value::JSValue::from_bits(bits).as_int32(); return (index >= 0).then_some(index as u32); } @@ -1217,7 +1219,7 @@ fn finite_nonnegative_u32_index(index: f64) -> Option { fn finite_nonnegative_i32_index(index: f64) -> Option { let bits = index.to_bits(); - if (bits & TAG_MASK) == INT32_TAG { + if crate::value::is_int32_bits(bits) { let index = crate::value::JSValue::from_bits(bits).as_int32(); return (index >= 0).then_some(index); } diff --git a/crates/perry-runtime/src/typed_feedback/tests.rs b/crates/perry-runtime/src/typed_feedback/tests.rs index 394c67909b0..3b8da1bfae7 100644 --- a/crates/perry-runtime/src/typed_feedback/tests.rs +++ b/crates/perry-runtime/src/typed_feedback/tests.rs @@ -2671,3 +2671,23 @@ fn representation_change_takes_no_registry_lock_with_feedback_off() { reset_typed_feedback_for_tests(); }); } + +/// 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_no_index_and_no_int32_kind() { + for bits in [PROTO_REF_5, HIGH_BITS] { + assert_eq!(finite_nonnegative_u32_index(f64::from_bits(bits)), None); + assert_eq!(finite_nonnegative_i32_index(f64::from_bits(bits)), None); + assert_ne!(stable_value_kind(bits), STABLE_VALUE_INT32); + } + assert_eq!( + finite_nonnegative_u32_index(f64::from_bits(0x7FFE_0000_0000_0005)), + Some(5) + ); + assert_eq!(stable_value_kind(0x7FFE_0000_0000_0005), STABLE_VALUE_INT32); +} diff --git a/crates/perry-runtime/src/typedarray/construct.rs b/crates/perry-runtime/src/typedarray/construct.rs index 81f1b93408a..f02443cb018 100644 --- a/crates/perry-runtime/src/typedarray/construct.rs +++ b/crates/perry-runtime/src/typedarray/construct.rs @@ -50,7 +50,7 @@ pub extern "C" fn js_typed_array_new(kind: i32, val: f64) -> *mut TypedArrayHead let rooted = scope.root_nanbox_f64(val); return typed_array_new_from_heap_source(kind, &rooted); } - if top16 == 0x7FFE { + if crate::value::is_int32_bits(bits) { // INT32_TAG — lower 32 bits are the signed length. let n = (bits & 0xFFFF_FFFF) as i32; let len = typed_array_length_or_throw(n as f64); diff --git a/crates/perry-runtime/src/value/dyn_index.rs b/crates/perry-runtime/src/value/dyn_index.rs index cbf2a6a88e1..64ab83cd2b8 100644 --- a/crates/perry-runtime/src/value/dyn_index.rs +++ b/crates/perry-runtime/src/value/dyn_index.rs @@ -106,7 +106,7 @@ fn raw_i64_receiver_is_managed(addr: usize) -> bool { fn finite_nonnegative_i32_index(index: f64) -> Option { let bits = index.to_bits(); - if (bits & TAG_MASK) == INT32_TAG { + if is_int32_bits(bits) { let index = JSValue::from_bits(bits).as_int32(); return (index >= 0).then_some(index); } @@ -119,7 +119,7 @@ fn finite_nonnegative_i32_index(index: f64) -> Option { fn finite_nonnegative_u32_index(index: f64) -> Option { let bits = index.to_bits(); - if (bits & TAG_MASK) == INT32_TAG { + if is_int32_bits(bits) { let index = JSValue::from_bits(bits).as_int32(); return (index >= 0).then_some(index as u32); } diff --git a/crates/perry-runtime/src/value/dyn_index_uint8array_tests.rs b/crates/perry-runtime/src/value/dyn_index_uint8array_tests.rs index f31d7202cc1..4f39adecfa3 100644 --- a/crates/perry-runtime/src/value/dyn_index_uint8array_tests.rs +++ b/crates/perry-runtime/src/value/dyn_index_uint8array_tests.rs @@ -24,3 +24,27 @@ fn dynamic_uint8array_set_coerces_nanboxed_values_before_narrowing() { js_dyn_index_set_strict(receiver, index_one, boxed_i32(-1), 1); assert_eq!(js_dyn_index_get(receiver, index_one), 255.0); } + +/// 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_no_dynamic_index() { + for bits in [PROTO_REF_5, HIGH_BITS] { + assert_eq!( + super::finite_nonnegative_u32_index(f64::from_bits(bits)), + None + ); + assert_eq!( + super::finite_nonnegative_i32_index(f64::from_bits(bits)), + None + ); + } + assert_eq!( + super::finite_nonnegative_u32_index(f64::from_bits(0x7FFE_0000_0000_0005)), + Some(5) + ); +} diff --git a/crates/perry-runtime/src/value/int32_exact_tests.rs b/crates/perry-runtime/src/value/int32_exact_tests.rs new file mode 100644 index 00000000000..1ba004b6483 --- /dev/null +++ b/crates/perry-runtime/src/value/int32_exact_tests.rs @@ -0,0 +1,40 @@ +//! The one int32-Number predicate (`tags::is_int32_bits`) and the shared +//! consumers built on it: a `0x7FFE` word with any of bits 32..47 set is not +//! an int32 Number. + +/// 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; + +const INT32_5: u64 = 0x7FFE_0000_0000_0005; + +#[test] +fn only_a_clear_high_payload_is_an_int32_number() { + assert!(super::is_int32_bits(INT32_5)); + assert!(super::JSValue::from_bits(INT32_5).is_int32()); + for bits in [PROTO_REF_5, HIGH_BITS] { + assert!(!super::is_int32_bits(bits)); + assert!(!super::JSValue::from_bits(bits).is_int32()); + } +} + +#[test] +fn a_prototype_ref_word_is_truthy_and_no_integer_key() { + // Class id 0 in the payload: a top-16-only test reads int32 0 (falsy). + let proto_ref_0 = 0x7FFE_0001_0000_0000u64; + assert_eq!(super::js_is_truthy(f64::from_bits(proto_ref_0)), 1); + assert_eq!( + super::js_is_truthy(f64::from_bits(0x7FFE_0000_0000_0000)), + 0 + ); + assert_eq!( + crate::object::field_get_set::runtime_read_site::integer_key(f64::from_bits(PROTO_REF_5)), + None + ); + assert_eq!( + crate::object::field_get_set::runtime_read_site::integer_key(f64::from_bits(INT32_5)), + Some(5) + ); +} diff --git a/crates/perry-runtime/src/value/jsvalue.rs b/crates/perry-runtime/src/value/jsvalue.rs index fe8bc7088e4..71872948b1b 100644 --- a/crates/perry-runtime/src/value/jsvalue.rs +++ b/crates/perry-runtime/src/value/jsvalue.rs @@ -123,7 +123,7 @@ impl JSValue { /// Check if this is an int32 #[inline] pub fn is_int32(&self) -> bool { - (self.bits & !INT32_MASK) == INT32_TAG + super::tags::is_int32_bits(self.bits) } /// A `POINTER_TAG` word: a GC-headed heap allocation whose header may be diff --git a/crates/perry-runtime/src/value/mod.rs b/crates/perry-runtime/src/value/mod.rs index d09c8a3f9f7..6ed1e072c80 100644 --- a/crates/perry-runtime/src/value/mod.rs +++ b/crates/perry-runtime/src/value/mod.rs @@ -62,7 +62,7 @@ mod tests; // named constant for documentation + external tooling but not re-exported // since no in-crate consumer references it directly. pub(crate) use tags::{ - BIGINT_TAG, INT32_MASK, INT32_TAG, JS_HANDLE_TAG, POINTER_MASK, POINTER_TAG, + is_int32_bits, BIGINT_TAG, INT32_MASK, INT32_TAG, JS_HANDLE_TAG, POINTER_MASK, POINTER_TAG, SHORT_STRING_DATA_MASK, SHORT_STRING_LEN_MASK, SHORT_STRING_LEN_SHIFT, SHORT_STRING_TAG, STRING_TAG, TAG_FALSE, TAG_HOLE, TAG_MARKER, TAG_MASK, TAG_NULL, TAG_TDZ, TAG_TRUE, TAG_UNDEFINED, @@ -92,6 +92,9 @@ pub(crate) use tags::{ JS_HANDLE_OBJECT_GET_PROPERTY, JS_HANDLE_TO_STRING, }; +#[cfg(test)] +mod int32_exact_tests; + // ----- JSValue type + impls ----- pub use jsvalue::JSValue; diff --git a/crates/perry-runtime/src/value/tags.rs b/crates/perry-runtime/src/value/tags.rs index 87ecc5d7f6f..619aa9a70bb 100644 --- a/crates/perry-runtime/src/value/tags.rs +++ b/crates/perry-runtime/src/value/tags.rs @@ -64,6 +64,16 @@ pub const fn is_reference_bits(bits: u64) -> bool { pub(crate) const INT32_TAG: u64 = 0x7FFE_0000_0000_0000; pub(crate) const INT32_MASK: u64 = 0x0000_0000_FFFF_FFFF; +/// THE "is this an int32 Number?" test: tag `0x7FFE` with bits 32..47 clear. +/// Other words in the `0x7FFE` band are not Numbers: the `C.prototype` ref +/// (`CLASS_PROTOTYPE_REF_FLAG`, bit 32), key-attribute and overflow route +/// words, the bound-closure inline-args marker. A test of the top 16 bits +/// alone reads those as int32 Numbers; every Number consumer asks this. +#[inline] +pub(crate) const fn is_int32_bits(bits: u64) -> bool { + bits & !INT32_MASK == INT32_TAG +} + /// String pointer tag: 0x7FFF_XXXX_XXXX_XXXX (48 bits for string pointer) pub(crate) const STRING_TAG: u64 = 0x7FFF_0000_0000_0000; diff --git a/crates/perry-runtime/src/value/truthy.rs b/crates/perry-runtime/src/value/truthy.rs index 1c7c67cd67f..42651e1c5de 100644 --- a/crates/perry-runtime/src/value/truthy.rs +++ b/crates/perry-runtime/src/value/truthy.rs @@ -70,10 +70,14 @@ pub extern "C" fn js_is_truthy(value: f64) -> i32 { } // Check for int32 tag - if (bits & TAG_MASK) == INT32_TAG { + if is_int32_bits(bits) { let int_val = (bits & INT32_MASK) as i32; return if int_val == 0 { 0 } else { 1 }; } + // Any other `0x7FFE` word (a `C.prototype` ref) is an object: truthy. + if (bits & TAG_MASK) == INT32_TAG { + return 1; + } // Check for SHORT_STRING_TAG (inline SSO strings): falsy iff length is 0. // Without this branch SSO empties would fall through to the f64 path, diff --git a/crates/perry-runtime/src/wpfacts_check.rs b/crates/perry-runtime/src/wpfacts_check.rs index ce407fb10c2..03b35782d61 100644 --- a/crates/perry-runtime/src/wpfacts_check.rs +++ b/crates/perry-runtime/src/wpfacts_check.rs @@ -60,7 +60,9 @@ fn kind_of(bits: u64) -> u32 { } 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, @@ -438,6 +440,18 @@ mod tests { assert!(!is_plain_f64(tag | POINTER_MASK), "{tag:#018x}"); } } + /// 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(0x7FFE_0000_0000_0003), K_NUM); + assert_eq!(kind_of(PROTO_REF_5), K_OBJ); + assert_eq!(kind_of(HIGH_BITS), K_OBJ); + } } /// Raw metadata ABI used only by the compile-time verification mode. diff --git a/scripts/check_file_size.sh b/scripts/check_file_size.sh index 73ef1cff1de..f7eb273e690 100755 --- a/scripts/check_file_size.sh +++ b/scripts/check_file_size.sh @@ -36,13 +36,13 @@ crates/perry-runtime/src/map.rs 3040 # Representation-aware set core; further decomposition is tracked in #1435. crates/perry-runtime/src/set.rs 2347 # Representation feedback core; further decomposition is tracked in #1435. -crates/perry-runtime/src/typed_feedback.rs 2436 +crates/perry-runtime/src/typed_feedback.rs 2437 # Snapshot census runner (instr-only, #13935); split after #14259 lands (snap census owner). crates/perry-runtime/src/snap_census/runner.rs 3572 # Native ABI proof cases; test organization is a separate follow-up. crates/perry-codegen/tests/native_proof_regressions.rs 15680 # Representation feedback regression cases; test organization is a separate follow-up. -crates/perry-runtime/src/typed_feedback/tests.rs 2289 +crates/perry-runtime/src/typed_feedback/tests.rs 2304 # Lowering report schema and its emitters; split emitters in a follow-up. crates/perry/src/commands/compile/lowering_report.rs 2321 # CallInliner argument and return remapping share one walker; split the traversal in a follow-up.