diff --git a/crates/perry-codegen/src/expr/property_get.rs b/crates/perry-codegen/src/expr/property_get.rs index c82cd2f72a..7763d513af 100644 --- a/crates/perry-codegen/src/expr/property_get.rs +++ b/crates/perry-codegen/src/expr/property_get.rs @@ -31,7 +31,8 @@ use crate::type_analysis::{ is_url_search_params_expr, receiver_class_name, }; #[allow(unused_imports)] -use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; +use crate::block::LlBlock; +use crate::types::{DOUBLE, I1, I16, I32, I64, I8, PTR}; use super::property_get_names::{ is_headers_method_name, is_http_agent_method_name, is_http_client_request_method_name, @@ -55,6 +56,99 @@ use super::{ TypedFeedbackKind, }; +/// #5094 Phase 3b: emit an inline, mostly loop-invariant "is this the expected +/// monomorphic class instance with an intact raw-f64 slot?" check, returning the +/// `i1` SSA name. The caller branches to the direct slot-access block on `true` +/// and to the full runtime guard call on `false`, so any miss degrades to +/// today's exact behavior — a `false` is never unsafe, only slower. Emitting the +/// check inline (instead of an opaque guard call) lets LLVM LICM hoist the +/// loop-invariant shape/header loads out of hot loops so the per-iteration cost +/// collapses to the direct load/store (the `method_calls` win). +/// +/// This is only used for `requires_raw_f64` *data-field* sites — getters/setters +/// return on earlier lowering paths, so no accessor exists for this field. +/// `inline_ok == true` implies every condition the runtime guard's success +/// requires: +/// - receiver is a `POINTER_TAG` heap object (tag-checked before any deref); +/// - `GcHeader.obj_type == GC_TYPE_OBJECT (2)` and not forwarded (`!0x80`); +/// - `ObjectHeader.object_type == OBJECT_TYPE_REGULAR (1)`; +/// - `class_id` (@+4) and `keys_array` (@+16) match the compile-time class +/// shape ⇒ `field_index` is in bounds and the key matches by construction; +/// - `GC_LAYOUT_TYPED_RAW_F64_INTACT (0x1000)` set ⇒ the slot is raw-f64 with +/// no downgrade (the same bit 3a added; the per-slot mask is derived from the +/// same `class_typed_layout` that sets `requires_raw_f64`); +/// - no own descriptor installed (`OBJ_FLAG_HAS_DESCRIPTORS 0x800` clear) — a +/// prototype accessor is shadowed by the own data slot, so a direct access +/// stays correct; +/// - (set only, `value` provided) the value is not a NaN-boxed non-number, so a +/// raw-f64 store can never publish a pointer/string into a pointer-free slot. +pub(super) fn emit_inline_class_field_guard( + blk: &mut LlBlock, + recv_box: &str, + expected_class_id: &str, + expected_keys: &str, + value: Option<&str>, +) -> String { + let recv_bits = blk.bitcast_double_to_i64(recv_box); + // Receiver must be a POINTER_TAG NaN-boxed heap object before any deref. + let tag = blk.and( + I64, + &recv_bits, + &crate::nanbox::i64_literal(crate::nanbox::TAG_MASK), + ); + let is_obj_tag = blk.icmp_eq(I64, &tag, crate::nanbox::POINTER_TAG_I64); + let handle = blk.and(I64, &recv_bits, POINTER_MASK_I64); + let obj_ptr = blk.inttoptr(I64, &handle); + // GcHeader sits 8 bytes below the user pointer: obj_type u8@-8, gc_flags + // u8@-7, _reserved u16@-6. + let obj_type_ptr = blk.gep(I8, &obj_ptr, &[(I64, "-8")]); + let obj_type = blk.load(I8, &obj_type_ptr); + let type_ok = blk.icmp_eq(I8, &obj_type, "2"); // GC_TYPE_OBJECT + let gcflags_ptr = blk.gep(I8, &obj_ptr, &[(I64, "-7")]); + let gcflags = blk.load(I8, &gcflags_ptr); + let fwd = blk.and(I8, &gcflags, "-128"); // GC_FLAG_FORWARDED (0x80) + let not_fwd = blk.icmp_eq(I8, &fwd, "0"); + let reserved_ptr = blk.gep(I8, &obj_ptr, &[(I64, "-6")]); + let reserved = blk.load(I16, &reserved_ptr); + // ObjectHeader.object_type u32@+0 + let otype = blk.load(I32, &obj_ptr); + let otype_ok = blk.icmp_eq(I32, &otype, "1"); // OBJECT_TYPE_REGULAR + // class_id u32@+4 + let cid_ptr = blk.gep(I8, &obj_ptr, &[(I64, "4")]); + let cid = blk.load(I32, &cid_ptr); + let cid_ok = blk.icmp_eq(I32, &cid, expected_class_id); + // keys_array ptr@+16 + let keys_ptr = blk.gep(I8, &obj_ptr, &[(I64, "16")]); + let keys = blk.load(I64, &keys_ptr); + let keys_ok = blk.icmp_eq(I64, &keys, expected_keys); + // GC_LAYOUT_TYPED_RAW_F64_INTACT (0x1000) and OBJ_FLAG_HAS_DESCRIPTORS (0x800) + let intact_bits = blk.and(I16, &reserved, "4096"); + let intact_ok = blk.icmp_ne(I16, &intact_bits, "0"); + let has_desc = blk.and(I16, &reserved, "2048"); + let no_desc = blk.icmp_eq(I16, &has_desc, "0"); + + let mut ok = blk.and(I1, &is_obj_tag, &type_ok); + ok = blk.and(I1, &ok, ¬_fwd); + ok = blk.and(I1, &ok, &otype_ok); + ok = blk.and(I1, &ok, &cid_ok); + ok = blk.and(I1, &ok, &keys_ok); + ok = blk.and(I1, &ok, &intact_ok); + ok = blk.and(I1, &ok, &no_desc); + if let Some(val) = value { + // Reject NaN-boxed non-numbers: every non-number tag satisfies + // (bits & 0x7FF8_0000_0000_0000) == 0x7FF8_0000_0000_0000, while finite + // numbers and ±inf do not (worst case a number-NaN is rejected and falls + // to the guard). This never accepts a pointer/string, so a raw-f64 store + // cannot publish a heap pointer into a pointer-free slot. + let val_bits = blk.bitcast_double_to_i64(val); + let qnan = crate::nanbox::i64_literal(0x7FF8_0000_0000_0000); + let masked = blk.and(I64, &val_bits, &qnan); + let val_num_ok = blk.icmp_ne(I64, &masked, &qnan); + ok = blk.and(I1, &ok, &val_num_ok); + } + ok +} + fn class_has_computed_runtime_members(ctx: &FnCtx<'_>, class_name: &str) -> bool { ctx.classes .get(class_name) @@ -1575,36 +1669,56 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .as_ref() .is_some_and(crate::typed_shape::type_is_raw_f64_candidate); let requires_raw_f64_str = if requires_raw_f64 { "1" } else { "0" }; - let (obj_bits, obj_handle, key_raw, guard_ok) = { + let obj_bits = ctx.block().bitcast_double_to_i64(&recv_box); + let obj_handle = ctx.block().and(I64, &obj_bits, POINTER_MASK_I64); + let key_raw = { let blk = ctx.block(); - let obj_bits = blk.bitcast_double_to_i64(&recv_box); - let obj_handle = blk.and(I64, &obj_bits, POINTER_MASK_I64); let key_box = blk.load(DOUBLE, &key_handle_global); let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - let expected_keys = blk.load(I64, &format!("@{}", keys_global_name)); - let guard_ok = blk.call( - I32, - "js_typed_feedback_class_field_get_guard", - &[ - (I64, &site_id), - (DOUBLE, &recv_box), - (I32, &expected_class_id_str), - (I64, &expected_keys), - (I64, &key_raw), - (I32, &field_idx_str), - (I32, requires_raw_f64_str), - ], - ); - (obj_bits, obj_handle, key_raw, guard_ok) + blk.and(I64, &key_bits, POINTER_MASK_I64) }; - let guard_pass = ctx.block().icmp_ne(I32, &guard_ok, "0"); + let expected_keys = + ctx.block().load(I64, &format!("@{}", keys_global_name)); let fast_idx = ctx.new_block("class_field_get.fast"); let fallback_idx = ctx.new_block("class_field_get.fallback"); let merge_idx = ctx.new_block("class_field_get.merge"); let fast_label = ctx.block_label(fast_idx); let fallback_label = ctx.block_label(fallback_idx); let merge_label = ctx.block_label(merge_idx); + + // #5094 Phase 3b: gate the direct slot load with an + // inline, LICM-hoistable shape/layout check; only a miss + // pays the out-of-line guard call. Restricted to raw-f64 + // data fields (the verified pointer-free case). On a miss + // we fall through to today's exact guard path, so an + // inline `false` is never unsafe — only slower. + if requires_raw_f64 { + let inline_ok = emit_inline_class_field_guard( + ctx.block(), + &recv_box, + &expected_class_id_str, + &expected_keys, + None, + ); + let needguard_idx = ctx.new_block("class_field_get.needguard"); + let needguard_label = ctx.block_label(needguard_idx); + ctx.block().cond_br(&inline_ok, &fast_label, &needguard_label); + ctx.current_block = needguard_idx; + } + let guard_ok = ctx.block().call( + I32, + "js_typed_feedback_class_field_get_guard", + &[ + (I64, &site_id), + (DOUBLE, &recv_box), + (I32, &expected_class_id_str), + (I64, &expected_keys), + (I64, &key_raw), + (I32, &field_idx_str), + (I32, requires_raw_f64_str), + ], + ); + let guard_pass = ctx.block().icmp_ne(I32, &guard_ok, "0"); ctx.block() .cond_br(&guard_pass, &fast_label, &fallback_label); diff --git a/crates/perry-codegen/src/expr/property_set.rs b/crates/perry-codegen/src/expr/property_set.rs index b2dee78e3e..234625ec7b 100644 --- a/crates/perry-codegen/src/expr/property_set.rs +++ b/crates/perry-codegen/src/expr/property_set.rs @@ -20,6 +20,7 @@ use crate::lower_string_method::{ lower_string_concat_chain, lower_string_self_append, }; #[allow(unused_imports)] +use super::property_get::emit_inline_class_field_guard; use crate::nanbox::{double_literal, POINTER_MASK_I64}; use crate::native_value::{ BoundsState, BufferAccessMode, LoweredValue, MaterializationReason, NativeRep, SemanticKind, @@ -310,35 +311,55 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { .as_ref() .is_some_and(crate::typed_shape::type_is_raw_f64_candidate); let requires_raw_f64_str = if requires_raw_f64 { "1" } else { "0" }; - let (key_raw, guard_ok) = { + let key_raw = { let blk = ctx.block(); let key_box = blk.load(DOUBLE, &key_handle_global); let key_bits = blk.bitcast_double_to_i64(&key_box); - let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64); - let expected_keys = blk.load(I64, &format!("@{}", keys_global_name)); - let guard_ok = blk.call( - I32, - "js_typed_feedback_class_field_set_guard", - &[ - (I64, &site_id), - (DOUBLE, &recv_box), - (I32, &expected_class_id_str), - (I64, &expected_keys), - (I64, &key_raw), - (I32, &field_idx_str), - (DOUBLE, &val_double), - (I32, requires_raw_f64_str), - ], - ); - (key_raw, guard_ok) + blk.and(I64, &key_bits, POINTER_MASK_I64) }; - let guard_pass = ctx.block().icmp_ne(I32, &guard_ok, "0"); + let expected_keys = + ctx.block().load(I64, &format!("@{}", keys_global_name)); let fast_idx = ctx.new_block("class_field_set.fast"); let fallback_idx = ctx.new_block("class_field_set.fallback"); let merge_idx = ctx.new_block("class_field_set.merge"); let fast_label = ctx.block_label(fast_idx); let fallback_label = ctx.block_label(fallback_idx); let merge_label = ctx.block_label(merge_idx); + + // #5094 Phase 3b: gate the direct raw-f64 store with an + // inline, LICM-hoistable shape/layout check plus an + // inline "value is a plain number" check; only a miss + // pays the out-of-line guard call. Restricted to raw-f64 + // data fields. On a miss we fall through to today's exact + // guard path, so an inline `false` is never unsafe. + if requires_raw_f64 { + let inline_ok = emit_inline_class_field_guard( + ctx.block(), + &recv_box, + &expected_class_id_str, + &expected_keys, + Some(&val_double), + ); + let needguard_idx = ctx.new_block("class_field_set.needguard"); + let needguard_label = ctx.block_label(needguard_idx); + ctx.block().cond_br(&inline_ok, &fast_label, &needguard_label); + ctx.current_block = needguard_idx; + } + let guard_ok = ctx.block().call( + I32, + "js_typed_feedback_class_field_set_guard", + &[ + (I64, &site_id), + (DOUBLE, &recv_box), + (I32, &expected_class_id_str), + (I64, &expected_keys), + (I64, &key_raw), + (I32, &field_idx_str), + (DOUBLE, &val_double), + (I32, requires_raw_f64_str), + ], + ); + let guard_pass = ctx.block().icmp_ne(I32, &guard_ok, "0"); ctx.block() .cond_br(&guard_pass, &fast_label, &fallback_label); diff --git a/crates/perry-runtime/src/gc/layout.rs b/crates/perry-runtime/src/gc/layout.rs index 52d470b312..7a52d9ee97 100644 --- a/crates/perry-runtime/src/gc/layout.rs +++ b/crates/perry-runtime/src/gc/layout.rs @@ -232,8 +232,23 @@ pub(super) unsafe fn header_from_user_ptr(user_ptr: *const u8) -> *mut GcHeader #[inline] pub(super) unsafe fn set_layout_state(header: *mut GcHeader, state: u16) { - (*header)._reserved = - ((*header)._reserved & !GC_LAYOUT_STATE_MASK) | (state & GC_LAYOUT_STATE_MASK); + // Any layout-state transition is also a descriptor-lifecycle event: every + // downgrade/removal path routes through here, so clearing the + // `GC_LAYOUT_TYPED_RAW_F64_INTACT` fast-path bit here keeps it strictly in + // lockstep with descriptor presence (#5094). The two install sites re-set + // it explicitly *after* their `set_layout_state` call, and `layout_transfer` + // re-applies it after moving the descriptor; nothing else sets it. + (*header)._reserved = ((*header)._reserved & !(GC_LAYOUT_STATE_MASK | GC_LAYOUT_TYPED_RAW_F64_INTACT)) + | (state & GC_LAYOUT_STATE_MASK); +} + +#[inline] +unsafe fn set_typed_raw_f64_intact(header: *mut GcHeader, on: bool) { + if on { + (*header)._reserved |= GC_LAYOUT_TYPED_RAW_F64_INTACT; + } else { + (*header)._reserved &= !GC_LAYOUT_TYPED_RAW_F64_INTACT; + } } #[inline] @@ -352,6 +367,13 @@ pub(crate) fn layout_clear_for_ptr(user_ptr: usize) { TYPED_LAYOUTS.with(|m| { m.borrow_mut().remove(&user_ptr); }); + // This path (sweep/free/reset) bypasses `set_layout_state`, so clear the + // fast-path bit directly to keep it in lockstep with descriptor removal. + unsafe { + if let Some(header) = layout_header_for_user(user_ptr) { + set_typed_raw_f64_intact(header, false); + } + } } pub(crate) fn layout_has_typed_descriptor(user_ptr: usize) -> bool { @@ -515,6 +537,7 @@ unsafe fn init_typed_shape_layout( } } + let has_raw_f64 = !raw_f64_mask.is_empty(); let descriptor = TypedLayoutDescriptor { slot_count, raw_f64_mask, @@ -534,6 +557,8 @@ unsafe fn init_typed_shape_layout( m.borrow_mut().insert(user_ptr, pointer_mask); }); } + // Re-set *after* the `set_layout_state` calls above (which clear it). + set_typed_raw_f64_intact(header, has_raw_f64); } #[no_mangle] @@ -619,6 +644,7 @@ pub extern "C" fn js_gc_init_unboxed_object_layout( } } + let has_raw_f64 = !raw_f64_mask.is_empty(); let descriptor = TypedLayoutDescriptor { slot_count, raw_f64_mask, @@ -638,6 +664,8 @@ pub extern "C" fn js_gc_init_unboxed_object_layout( m.borrow_mut().insert(user_ptr, pointer_mask); }); } + // Re-set *after* the `set_layout_state` calls above (which clear it). + set_typed_raw_f64_intact(header, has_raw_f64); } } @@ -712,6 +740,9 @@ pub(crate) unsafe fn layout_transfer(old_user: *mut u8, new_user: *mut u8) { return; }; let state = (*old_header)._reserved & GC_LAYOUT_STATE_MASK; + // Capture the fast-path bit before `set_layout_state(new_header, ...)` clears + // it on the destination; re-apply it below once the descriptor has moved. + let typed_raw_f64_intact = (*old_header)._reserved & GC_LAYOUT_TYPED_RAW_F64_INTACT != 0; set_layout_state(new_header, state); if (*old_header).obj_type == GC_TYPE_ARRAY && (*new_header).obj_type == GC_TYPE_ARRAY { crate::array::transfer_array_numeric_layout(old_user as usize, new_user as usize); @@ -732,6 +763,7 @@ pub(crate) unsafe fn layout_transfer(old_user: *mut u8, new_user: *mut u8) { masks.insert(new_user as usize, mask); } }); + set_typed_raw_f64_intact(new_header, typed_raw_f64_intact); } pub(super) fn layout_visit_pointer_slots( @@ -778,6 +810,61 @@ pub(crate) fn layout_typed_raw_f64_slot_for_user(user_ptr: usize, slot_index: us }) } +#[inline] +fn verify_layout_fastpath_enabled() -> bool { + static FLAG: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0); + let cached = FLAG.load(std::sync::atomic::Ordering::Relaxed); + if cached != 0 { + return cached == 2; + } + let on = std::env::var("PERRY_VERIFY_LAYOUT_FASTPATH") + .map(|v| matches!(v.as_str(), "1" | "on" | "true")) + .unwrap_or(false); + FLAG.store(if on { 2 } else { 1 }, std::sync::atomic::Ordering::Relaxed); + on +} + +/// Guard-only fast path for "is this class field a raw-`f64` slot?" (#5094). +/// +/// PRECONDITION (enforced by every caller): the call site's compile-time +/// `require_raw_f64` flag is true. By construction in codegen +/// (`typed_shape::class_typed_layout` / `type_is_raw_f64_candidate`), that flag +/// is set for exactly the fields the canonical class layout records in the +/// descriptor's `raw_f64_mask`. So when the object still carries an intact +/// typed descriptor (`GC_LAYOUT_TYPED_RAW_F64_INTACT`), the answer is "yes" for +/// any in-bounds slot — resolvable from the GC header + the object's +/// `field_count` with no thread-local `TYPED_LAYOUTS` lookup. This removes the +/// per-access `_tlv_get_addr` + hashmap touch that dominates `method_calls`. +/// +/// On a cleared bit (no descriptor, or downgraded) it falls back to the precise +/// per-slot predicate, preserving today's behavior byte-for-byte. +/// +/// `PERRY_VERIFY_LAYOUT_FASTPATH=1` cross-checks the fast answer against the +/// precise predicate on every fast-path hit and panics on divergence — this is +/// the empirical guard against any codegen/runtime mask-derivation skew. The +/// same check runs unconditionally under `debug_assertions`. +pub(crate) fn layout_guard_field_is_raw_f64(user_ptr: usize, slot_index: usize) -> bool { + unsafe { + if let Some(header) = layout_header_for_user(user_ptr) { + if (*header)._reserved & GC_LAYOUT_TYPED_RAW_F64_INTACT != 0 { + let obj = user_ptr as *const crate::object::ObjectHeader; + let in_bounds = slot_index < (*obj).field_count as usize; + if cfg!(debug_assertions) || verify_layout_fastpath_enabled() { + let precise = layout_typed_raw_f64_slot_for_user(user_ptr, slot_index); + assert_eq!( + in_bounds, precise, + "layout fast-path divergence: GC_LAYOUT_TYPED_RAW_F64_INTACT set but \ + slot {slot_index} (field_count {}) disagrees with raw_f64_mask", + (*obj).field_count + ); + } + return in_bounds; + } + } + } + layout_typed_raw_f64_slot_for_user(user_ptr, slot_index) +} + fn layout_typed_raw_f64_slot_count_for_user(user_ptr: usize, slot_count: usize) -> usize { TYPED_LAYOUTS.with(|m| { m.borrow() diff --git a/crates/perry-runtime/src/gc/types.rs b/crates/perry-runtime/src/gc/types.rs index ced071c3e0..75974f4da4 100644 --- a/crates/perry-runtime/src/gc/types.rs +++ b/crates/perry-runtime/src/gc/types.rs @@ -818,6 +818,18 @@ pub const OBJ_FLAG_ARRAY_DESCRIPTORS: u16 = 0x400; // `GC_TYPE_OBJECT`. Set-only (clearing a descriptor leaves it set; the slow // path is always correct). pub const OBJ_FLAG_HAS_DESCRIPTORS: u16 = 0x800; +// #5094: this `GC_TYPE_OBJECT`'s typed-shape layout descriptor is installed and +// intact (no downgrade observed) AND has at least one raw-`f64` slot. When set, +// a class-field guard whose compile-time `require_raw_f64` flag is true can +// trust the accessed slot is raw-`f64` without the per-access thread-local +// `TYPED_LAYOUTS` hashmap lookup — codegen only emits `require_raw_f64=1` for +// fields the canonical class layout marks `number`, which are exactly the +// descriptor's `raw_f64_mask` members. Cleared the instant the descriptor is +// removed or downgraded (every such transition routes through +// `set_layout_state`). Bit 12; only meaningful for `GC_TYPE_OBJECT`. The +// per-slot predicate `layout_typed_raw_f64_slot_for_user` is unchanged; only +// the guard-only fast path `layout_guard_field_is_raw_f64` consults this bit. +pub(crate) const GC_LAYOUT_TYPED_RAW_F64_INTACT: u16 = 0x1000; // #2145: this object is a per-kind `.prototype` whose // `[[Prototype]]` is the shared `%TypedArray%.prototype` intrinsic. // `Object.getPrototypeOf(Int8Array.prototype)` returns the cached diff --git a/crates/perry-runtime/src/typed_feedback/guards.rs b/crates/perry-runtime/src/typed_feedback/guards.rs index f37ba8fc1f..14ced3c151 100644 --- a/crates/perry-runtime/src/typed_feedback/guards.rs +++ b/crates/perry-runtime/src/typed_feedback/guards.rs @@ -271,7 +271,7 @@ fn class_field_get_contract( && plain_array_index_guard(expected_keys, expected_field_index, true) && object_key_matches_field(obj, key, expected_field_index) && (!require_raw_f64 - || crate::gc::layout_typed_raw_f64_slot_for_user( + || crate::gc::layout_guard_field_is_raw_f64( object_addr, expected_field_index as usize, )) @@ -307,7 +307,7 @@ fn class_field_fast_contract( && std::ptr::eq((*obj).keys_array as *const ArrayHeader, expected_keys) && expected_field_index < (*obj).field_count && (!require_raw_f64 - || crate::gc::layout_typed_raw_f64_slot_for_user( + || crate::gc::layout_guard_field_is_raw_f64( object_addr, expected_field_index as usize, )) @@ -474,7 +474,7 @@ fn class_field_set_contract( && object_key_matches_field(obj, key, expected_field_index) && (!require_raw_f64 || (is_plain_number_bits(value_bits) - && crate::gc::layout_typed_raw_f64_slot_for_user( + && crate::gc::layout_guard_field_is_raw_f64( object_addr, expected_field_index as usize, ))) diff --git a/test-files/test_issue_5094_layout_fastpath.ts b/test-files/test_issue_5094_layout_fastpath.ts new file mode 100644 index 0000000000..7f40c4103e --- /dev/null +++ b/test-files/test_issue_5094_layout_fastpath.ts @@ -0,0 +1,62 @@ +// #5094: O(1) class-field raw-f64 layout fast path. +// Exercises the memory-corruption-risk cases the header-bit fast path must +// handle: (a) downgrade a number field via an `any` alias then read it back, +// (b) a mixed pointer+number class, (c) GC churn mid-access. + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} + +const p = new Point(1, 2); + +// Hot read/write loop — drives the fast path. +let sum = 0; +for (let i = 0; i < 200000; i++) { + p.x = p.x + 1; + sum = sum + p.y; +} +console.log("sum:" + sum); +console.log("px:" + p.x); + +// (a) Downgrade x to a string via an `any` alias. The canonical bit must clear +// so the subsequent read does NOT reinterpret the string pointer as a double. +const a: any = p; +a.x = "downgraded"; +console.log("after-downgrade-x:" + p.x); +console.log("typeof-x:" + typeof a.x); +// y is untouched and must still read as a number. +console.log("after-downgrade-y:" + (p.y + 40)); + +// (b) Mixed class: number + pointer fields. +class Mixed { + n: number; + label: string; + constructor(n: number, label: string) { + this.n = n; + this.label = label; + } +} +const m = new Mixed(7, "tag"); +let acc = 0; +for (let i = 0; i < 200000; i++) { + m.n = m.n + 2; + acc = acc + m.n; +} +console.log("mixed-n:" + m.n); +console.log("mixed-label:" + m.label); +console.log("mixed-acc-mod:" + (acc % 1000)); + +// (c) GC churn: allocate many Points while reading fields of a survivor. +const survivor = new Point(100, 200); +let g = 0; +for (let i = 0; i < 100000; i++) { + const tmp = new Point(i, i + 1); + g = g + tmp.x + survivor.x; +} +console.log("survivor-x:" + survivor.x); +console.log("g-mod:" + (g % 7));