diff --git a/changelog.d/11588-untyped-array-element-access.md b/changelog.d/11588-untyped-array-element-access.md new file mode 100644 index 0000000000..f3b3cd5636 --- /dev/null +++ b/changelog.d/11588-untyped-array-element-access.md @@ -0,0 +1 @@ +perf(codegen): an untyped `obj[i] = v` onto a live ordinary Array now stores inline behind the guarded in-bounds tier instead of calling `js_dyn_index_set_strict` on every element (#10513). Untyped reads heal one growth-forwarding hop inline, and the packed-f64 loop tier repairs a forwarded receiver slot before its guard (#10514). On qb2: node-forge/rsa_sign −50.4%, big.js/arith_chain −42.6% instructions per iteration; the #10514 grown-array read goes from 564 to 73 instructions per element. diff --git a/crates/perry-codegen/src/expr/barrier_stem_census_tests.rs b/crates/perry-codegen/src/expr/barrier_stem_census_tests.rs index 2c24c3206f..7f60a1a4bc 100644 --- a/crates/perry-codegen/src/expr/barrier_stem_census_tests.rs +++ b/crates/perry-codegen/src/expr/barrier_stem_census_tests.rs @@ -91,6 +91,7 @@ pub(super) const VERIFIED_BARRIER_STEMS: &[(&str, StemKind)] = &[ ("apush", StemKind::GenerationTested), ("class_field_set", StemKind::PointerTestedStore), ("ctor_prologue", StemKind::ValueAndGenerationTested), + ("dynarr.set", StemKind::ValueAndGenerationTested), ("idxset.inbounds", StemKind::ValueAndGenerationTested), ("idxset.recv_captured", StemKind::ValueAndGenerationTested), ("idxset.recv_global", StemKind::ValueAndGenerationTested), @@ -798,6 +799,49 @@ fn idxset_runtime_key_ir() -> String { .expect("LLVM IR should be UTF-8") } +/// `probe(a: any, k: any, v: any) { a[k] = v }` — the untyped store's +/// ordinary-Array arm (#10513), whose inline in-bounds store must keep the +/// value-and-generation-tested barrier for an arbitrary value. +fn dynarr_set_ir() -> String { + const ARR_ID: u32 = 31; + let mut m = Module::new("dynarr_set_census.ts"); + let param = |id: u32, name: &str| Param { + id, + name: name.to_string(), + ty: Type::Any, + default: None, + decorators: Vec::new(), + is_rest: false, + arguments_object: None, + }; + m.functions = vec![Function { + id: 1, + name: "probe".to_string(), + type_params: Vec::new(), + params: vec![param(ARR_ID, "a"), param(IDX_ID, "k"), param(VAL_ID, "v")], + return_type: Type::Any, + body: vec![ + Stmt::Expr(Expr::IndexSet { + object: Box::new(Expr::LocalGet(ARR_ID)), + index: Box::new(Expr::LocalGet(IDX_ID)), + value: Box::new(Expr::LocalGet(VAL_ID)), + }), + Stmt::Return(Some(Expr::LocalGet(ARR_ID))), + ], + is_async: false, + is_generator: false, + is_strict: true, + is_exported: false, + captures: Vec::new(), + decorators: Vec::new(), + was_plain_async: false, + was_unrolled: false, + }]; + m.init_kind = ModuleInitKind::Eager; + String::from_utf8(compile_module(&m, ir_opts()).expect("module compiles")) + .expect("LLVM IR should be UTF-8") +} + /// `class Boxed { v: any; constructor(v) { this.v = v } }` plus an escaping /// `new Boxed(1)` — the complete parameter-to-field constructor is what selects /// constructor-free prologue stores, and the boxed field requires their @@ -813,6 +857,7 @@ fn probe_ir(stem: &str) -> String { "apush" => apush_ir(), "class_field_set" => super::class_field_barrier_tests::ir(), "ctor_prologue" => ctor_prologue_ir(), + "dynarr.set" => dynarr_set_ir(), "idxset.inbounds" => idxset_inbounds_ir(), "idxset.recv_captured" => idxset_recv_captured_ir(), "idxset.recv_global" => idxset_recv_global_ir(), diff --git a/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs b/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs index 00d9cedb4c..dd1b70d0c6 100644 --- a/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs +++ b/crates/perry-codegen/src/expr/index_get/inline_dyn_typed_array.rs @@ -119,6 +119,12 @@ pub(super) fn lower_inline_dyn_typed_array_get( let elem_bounds_idx = ctx.new_block("arrlike.elem.bounds"); let elem_load_idx = ctx.new_block("arrlike.elem.load"); let elem_value_idx = ctx.new_block("arrlike.elem.value"); + let fwd_check_idx = ctx.new_block("arrlike.ic.fwd_check"); + let fwd_follow_idx = ctx.new_block("arrlike.ic.fwd_follow"); + let fwd_header_idx = ctx.new_block("arrlike.ic.fwd_header"); + let fwd_check_label = ctx.block_label(fwd_check_idx); + let fwd_follow_label = ctx.block_label(fwd_follow_idx); + let fwd_header_label = ctx.block_label(fwd_header_idx); let object_miss_idx = ctx.new_block("arrlike.ic.miss"); let merge_idx = ctx.new_block("arrlike.ic.merge"); let object_header_label = ctx.block_label(object_header_idx); @@ -185,8 +191,60 @@ pub(super) fn lower_inline_dyn_typed_array_get( let forwarded = ctx.block().and(I8, &gc_flags, "128"); let not_forwarded = ctx.block().icmp_eq(I8, &forwarded, "0"); let header_ok = ctx.block().and(I1, &object_idx_is_int, ¬_forwarded); + let header_end_label = ctx.block().label.clone(); ctx.block() - .cond_br(&header_ok, &object_brand_label, &object_miss_label); + .cond_br(&header_ok, &object_brand_label, &fwd_check_label); + + // #10514: heal ONE growth-forwarding hop inline. An Array that outgrew its + // storage leaves a forwarding stub at the old head, and every binding that + // is not the grown local itself — an object field (`this.data`, jsbn's + // BigInteger digits), a module global, a closure capture, a parameter — + // keeps that stub forever. Rejecting the stub sent every later read of such + // an array out of line through `js_packed_arraylike_index_get` → + // `js_array_get_f64`, which classifies the address again and follows the + // chain on every read (~470 instructions per element vs ~70 presized). + // The guarded store tier and `guarded_array.rs` already follow this edge: + // the stub's first payload word is the live user address. It is trusted + // only once it is in the heap band and its own header re-brands as a + // non-forwarded `GC_TYPE_ARRAY`; a longer or corrupt chain, and every + // forwarded non-Array, keeps the unchanged slow exit. + ctx.current_block = fwd_check_idx; + let follow = { + let blk = ctx.block(); + let is_forwarded = blk.icmp_ne(I8, &forwarded, "0"); + let forwarded_array = blk.and(I1, &is_array, &is_forwarded); + blk.and(I1, &forwarded_array, &object_idx_is_int) + }; + ctx.block() + .cond_br(&follow, &fwd_follow_label, &object_miss_label); + + ctx.current_block = fwd_follow_idx; + let fwd_target = { + let blk = ctx.block(); + let stub_ptr = blk.inttoptr(I64, &object_raw); + let target = blk.load(I64, &stub_ptr); + let above_floor = blk.icmp_uge(I64, &target, &heap_floor); + let below_ceiling = blk.icmp_ult(I64, &target, &heap_ceiling); + let in_band = blk.and(I1, &above_floor, &below_ceiling); + blk.cond_br(&in_band, &fwd_header_label, &object_miss_label); + target + }; + + ctx.current_block = fwd_header_idx; + { + let blk = ctx.block(); + let type_addr = blk.sub(I64, &fwd_target, "8"); + let type_ptr = blk.inttoptr(I64, &type_addr); + let live_type = blk.load(I8, &type_ptr); + let live_is_array = blk.icmp_eq(I8, &live_type, "1"); + let flags_addr = blk.sub(I64, &fwd_target, "7"); + let flags_ptr = blk.inttoptr(I64, &flags_addr); + let live_flags = blk.load(I8, &flags_ptr); + let live_forwarded = blk.and(I8, &live_flags, "128"); + let live_not_forwarded = blk.icmp_eq(I8, &live_forwarded, "0"); + let live_ok = blk.and(I1, &live_is_array, &live_not_forwarded); + blk.cond_br(&live_ok, &object_brand_label, &object_miss_label); + } // `GC_TYPE_ARRAY` takes the direct guarded load. Everything else is offered // to the typed-array arm, then to the elements-backed Array-subclass @@ -194,7 +252,26 @@ pub(super) fn lower_inline_dyn_typed_array_get( // both brand tests and are classified by the slow exit. Both `tav.brand` // and `arrlike.elem.kind` re-test the brand they need before they read a // header word, so nothing else can reach those loads. + // + // From here on the receiver is `object_raw`: the original head, or the + // live head one forwarding hop away (which re-branded as an Array). ctx.current_block = object_brand_idx; + let fwd_header_label_s = ctx.block_label(fwd_header_idx); + let object_raw = ctx.block().phi( + I64, + &[ + (object_raw.as_str(), header_end_label.as_str()), + (fwd_target.as_str(), fwd_header_label_s.as_str()), + ], + ); + let gc_type = ctx.block().phi( + I8, + &[ + (gc_type.as_str(), header_end_label.as_str()), + ("1", fwd_header_label_s.as_str()), + ], + ); + let is_array = ctx.block().icmp_eq(I8, &gc_type, "1"); ctx.block() .cond_br(&is_array, &object_array_guard_label, &ta_brand_label); diff --git a/crates/perry-codegen/src/expr/index_get_claim_tests.rs b/crates/perry-codegen/src/expr/index_get_claim_tests.rs index f98f78ac6b..e95bea9576 100644 --- a/crates/perry-codegen/src/expr/index_get_claim_tests.rs +++ b/crates/perry-codegen/src/expr/index_get_claim_tests.rs @@ -215,6 +215,10 @@ fn unknown_numeric_read_is_one_inline_hit_and_one_out_of_line_exit() { "arrlike.elem.bounds", "arrlike.elem.load", "arrlike.elem.value", + // #10514: one growth-forwarding hop healed inline. + "arrlike.ic.fwd_check", + "arrlike.ic.fwd_follow", + "arrlike.ic.fwd_header", "arrlike.ic.miss", "arrlike.ic.merge", ], @@ -370,7 +374,7 @@ fn the_number_context_coercion_is_coupled_across_every_arm() { ); assert_eq!( dynamic_index_site_blocks(&ir).len(), - 21, + 24, "{name}: a number context must not change the emitted block shape:\n{ir}" ); let miss = super::class_field_barrier_tests::block_body(&ir, "arrlike.ic.miss.") diff --git a/crates/perry-codegen/src/expr/index_set.rs b/crates/perry-codegen/src/expr/index_set.rs index a173c1c793..002dac9b48 100644 --- a/crates/perry-codegen/src/expr/index_set.rs +++ b/crates/perry-codegen/src/expr/index_set.rs @@ -37,7 +37,7 @@ use crate::native_value::{ }; use crate::rooting; use crate::type_analysis::{is_array_expr, is_numeric_expr, is_string_expr, receiver_class_name}; -use crate::types::{DOUBLE, I1, I32, I64}; +use crate::types::{DOUBLE, I32, I64}; use super::index_set_packed_loop::lower_packed_numeric_loop_index_set; use super::index_set_typed_array::lower_inline_dyn_typed_array_set; @@ -319,28 +319,8 @@ fn lower_array_index_set_via_runtime_key( // side has done this since #7286 (`aidx.canonical`). A rejected key // becomes index -1, which the guarded in-bounds store declines // onto the same helper arm, so the helper is emitted once. - let guard_idx_i32 = { - let blk = ctx.block(); - let raw_ge_zero = blk.fcmp("oge", &idx_double, "0.0"); - let raw_le_i32_max = blk.fcmp("ole", &idx_double, "2147483647.0"); - let raw_in_range = blk.and(I1, &raw_ge_zero, &raw_le_i32_max); - // `fptosi` is poison for NaN/out-of-range input: convert the - // range-sanitized value. - let safe_raw = blk.select(I1, &raw_in_range, DOUBLE, &idx_double, "0.0"); - let raw_i32 = blk.fptosi(DOUBLE, &safe_raw, I32); - let raw_round_trip = blk.sitofp(I32, &raw_i32, DOUBLE); - let raw_is_integral = blk.fcmp("oeq", &raw_round_trip, &idx_double); - let raw_is_canonical = blk.and(I1, &raw_in_range, &raw_is_integral); - let bits = blk.bitcast_double_to_i64(&idx_double); - let top16 = blk.lshr(I64, &bits, "48"); - let is_boxed_i32 = blk.icmp_eq(I64, &top16, crate::nanbox::INT32_TAG_TOP16_I64); - let boxed_i32 = blk.trunc(I64, &bits, I32); - let boxed_nonnegative = blk.icmp_sge(I32, &boxed_i32, "0"); - let boxed_is_canonical = blk.and(I1, &is_boxed_i32, &boxed_nonnegative); - let canonical = blk.or(I1, &raw_is_canonical, &boxed_is_canonical); - let idx_i32 = blk.select(I1, &is_boxed_i32, I32, &boxed_i32, &raw_i32); - blk.select(I1, &canonical, I32, &idx_i32, "-1") - }; + let guard_idx_i32 = + super::index_set_guarded::emit_canonical_element_index_i32(ctx, &idx_double); super::index_set_guarded::emit_guarded_inbounds_array_store( ctx, &arr_box, @@ -670,7 +650,8 @@ pub(crate) fn lower( &vals[1], &vals[2], assignment_strict, - ); + None, + )?; let slow = LoweredValue::js_value(result.clone()); ctx.record_lowered_value_with_access_mode( "TypedArraySet", @@ -704,7 +685,8 @@ pub(crate) fn lower( &vals[1], &vals[2], assignment_strict, - ); + None, + )?; let slow = LoweredValue::js_value(vals[2].clone()); ctx.record_lowered_value_with_access_mode( "TypedArraySet", @@ -794,6 +776,13 @@ pub(crate) fn lower( ) || is_string_expr(ctx, index); if recv_unknown && !index_is_static_string_or_symbol { let strict = assignment_strict; + // #10513: the receiver's layout is unknown, so the layout note + // stays on; the barrier and numeric note follow the VALUE. + let array_facts = super::index_set_typed_array::DynArrayStoreFacts { + layout_note_needed: true, + write_barrier_needed: array_store_needs_write_barrier(ctx, value), + value_is_numeric: is_numeric_expr(ctx, value), + }; return rooting::with_operands_rooted_across( ctx, &[object, index], @@ -814,13 +803,14 @@ pub(crate) fn lower( // at the access site, falling back to `js_dyn_index_set` on // any guard miss. #7640: both the receiver and key are // re-read after the allocating RHS. - Ok(lower_inline_dyn_typed_array_set( + lower_inline_dyn_typed_array_set( ctx, &vals[0], &vals[1], &val_double, strict, - )) + Some(array_facts), + ) }, ); } diff --git a/crates/perry-codegen/src/expr/index_set_barrier_tests.rs b/crates/perry-codegen/src/expr/index_set_barrier_tests.rs index dce9fcb4c4..f3d4ea537c 100644 --- a/crates/perry-codegen/src/expr/index_set_barrier_tests.rs +++ b/crates/perry-codegen/src/expr/index_set_barrier_tests.rs @@ -428,32 +428,46 @@ fn the_guarded_property_receiver_store_follows_one_forwarding_edge_inline() { let ir = ir(); let deref = block_body(&ir, "idxset.recv_prop.deref.").expect("guarded store emits its `deref` block"); + let follow = block_body(&ir, "idxset.recv_prop.deref.follow.") + .expect("guarded store emits its `deref.follow` block"); let live = block_body(&ir, "idxset.recv_prop.deref.live.") .expect("guarded store emits its `deref.live` block"); let fast = block_body(&ir, "idxset.recv_prop.fast.").expect("guarded store emits its `fast` block"); - // (1) `deref` reads the stub's first payload word and selects it as the - // live handle when the header says ARRAY + FORWARDED. - let select_line = deref + // (0) #10513: `deref` decides on the ARRAY brand byte alone, so a receiver + // that is not an Array leaves before any forwarding or integrity work. + assert!( + deref.contains("sub i64") && deref.contains("load i8"), + "`deref` reads the brand byte:\n{deref}" + ); + assert!( + deref.contains("br i1") && deref.contains("idxset.recv_prop.deref.follow."), + "`deref` must branch into `deref.follow` on the ARRAY brand:\n{deref}" + ); + + // (1) `deref.follow` reads the stub's first payload word and selects it as + // the live handle when the header says FORWARDED. + let select_line = follow .lines() .map(str::trim) .find(|line| line.contains("select i1") && line.contains("i64")) - .expect("`deref` selects between the forwarding target and the receiver"); + .expect("`deref.follow` selects between the forwarding target and the receiver"); let live_handle = select_line .split(" = ") .next() .expect("select defines a register") .to_string(); let target = operand(select_line, 2).expect("select's taken operand"); - let target_def = def_of(&deref, &target).expect("forwarding target is defined in `deref`"); + let target_def = + def_of(&follow, &target).expect("forwarding target is defined in `deref.follow`"); assert!( target_def.contains("load i64"), "the forwarding target must be the stub's first payload word, got `{target_def}`" ); assert!( - deref.contains("br i1") && deref.contains("idxset.recv_prop.deref.live."), - "`deref` must branch into `deref.live` after the heap-band test of the live handle" + follow.contains("br i1") && follow.contains("idxset.recv_prop.deref.live."), + "`deref.follow` must branch into `deref.live` after the heap-band test of the live handle" ); // (2) `deref.live` re-reads the ARRAY brand and the FORWARDED bit from the diff --git a/crates/perry-codegen/src/expr/index_set_guarded.rs b/crates/perry-codegen/src/expr/index_set_guarded.rs index 5e619aede5..d210ed90df 100644 --- a/crates/perry-codegen/src/expr/index_set_guarded.rs +++ b/crates/perry-codegen/src/expr/index_set_guarded.rs @@ -41,17 +41,50 @@ use anyhow::Result; use crate::nanbox::POINTER_MASK_I64; -use crate::types::{I1, I16, I32, I64, I8}; +use crate::types::{DOUBLE, I1, I16, I32, I64, I8}; use super::write_barrier::{ - emit_jsvalue_slot_store_deferred_layout_note_on_block, emit_layout_note_slot_aware_on_block, - emit_layout_pointer_bearing_check, + emit_jsvalue_slot_store_deferred_layout_note_without_addref_on_block, + emit_layout_note_slot_aware_on_block, emit_layout_pointer_bearing_check, }; use super::{ emit_array_numeric_write_note_on_block, emit_jsvalue_slot_store_scalar_aware_on_block, emit_write_barrier_slot_value_and_generation_tested, FnCtx, }; +/// The canonical element index a DOUBLE key names, as an `i32`, or `-1` when +/// it names none (fractional, negative, non-finite, above `i32::MAX`, or any +/// NaN-boxed non-number such as a string or Symbol key). `-1` is exactly the +/// value [`emit_guarded_inbounds_array_store`]'s guard declines, so a caller +/// can hand every key to it and keep ONE slow arm for the rejected ones. +/// +/// Both numeric encodings are recognised: a plain double and an +/// `INT32_TAG`-boxed integer (a loop counter that was boxed on the way in). +/// The conversion never feeds `fptosi` an out-of-range or NaN input, which +/// would be poison. +pub(super) fn emit_canonical_element_index_i32(ctx: &mut FnCtx<'_>, idx_double: &str) -> String { + let blk = ctx.block(); + let raw_ge_zero = blk.fcmp("oge", idx_double, "0.0"); + let raw_le_i32_max = blk.fcmp("ole", idx_double, "2147483647.0"); + let raw_in_range = blk.and(I1, &raw_ge_zero, &raw_le_i32_max); + // `fptosi` is poison for NaN/out-of-range input: convert the + // range-sanitized value. + let safe_raw = blk.select(I1, &raw_in_range, DOUBLE, idx_double, "0.0"); + let raw_i32 = blk.fptosi(DOUBLE, &safe_raw, I32); + let raw_round_trip = blk.sitofp(I32, &raw_i32, DOUBLE); + let raw_is_integral = blk.fcmp("oeq", &raw_round_trip, idx_double); + let raw_is_canonical = blk.and(I1, &raw_in_range, &raw_is_integral); + let bits = blk.bitcast_double_to_i64(idx_double); + let top16 = blk.lshr(I64, &bits, "48"); + let is_boxed_i32 = blk.icmp_eq(I64, &top16, crate::nanbox::INT32_TAG_TOP16_I64); + let boxed_i32 = blk.trunc(I64, &bits, I32); + let boxed_nonnegative = blk.icmp_sge(I32, &boxed_i32, "0"); + let boxed_is_canonical = blk.and(I1, &is_boxed_i32, &boxed_nonnegative); + let canonical = blk.or(I1, &raw_is_canonical, &boxed_is_canonical); + let idx_i32 = blk.select(I1, &is_boxed_i32, I32, &boxed_i32, &raw_i32); + blk.select(I1, &canonical, I32, &idx_i32, "-1") +} + /// Emit the guarded diamond. `fallback` emits the original slow arm (the /// runtime call plus whatever bookkeeping it owns) into the block that is /// current when it runs. @@ -69,6 +102,42 @@ pub(super) fn emit_guarded_inbounds_array_store( write_barrier_needed: bool, value_is_numeric: bool, fallback: impl FnOnce(&mut FnCtx<'_>) -> Result<()>, +) -> Result<()> { + emit_guarded_inbounds_array_store_keyed( + ctx, + arr_box, + StoreIndex::I32(idx_i32), + val_double, + block_prefix, + layout_note_needed, + write_barrier_needed, + value_is_numeric, + fallback, + ) +} + +/// The key of a guarded store: an already-materialized `i32`, or a DOUBLE key +/// whose canonical element index ([`emit_canonical_element_index_i32`]) is +/// computed only once the receiver has branded as an Array, so a declining +/// receiver (the untyped route's typed arrays and Buffers) never pays for it. +#[derive(Clone, Copy)] +pub(super) enum StoreIndex<'a> { + I32(&'a str), + CanonicalOfDouble(&'a str), +} + +/// [`emit_guarded_inbounds_array_store`] with a [`StoreIndex`] key. +#[allow(clippy::too_many_arguments)] +pub(super) fn emit_guarded_inbounds_array_store_keyed( + ctx: &mut FnCtx<'_>, + arr_box: &str, + index: StoreIndex<'_>, + val_double: &str, + block_prefix: &str, + layout_note_needed: bool, + write_barrier_needed: bool, + value_is_numeric: bool, + fallback: impl FnOnce(&mut FnCtx<'_>) -> Result<()>, ) -> Result<()> { // An operand may have emitted a throw + unreachable. LlBlock drops // instructions after a terminator; opening the store diamond would then @@ -106,17 +175,30 @@ pub(super) fn emit_guarded_inbounds_array_store( } ctx.current_block = deref_idx; + let follow_idx = ctx.new_block(&format!("{}.deref.follow", block_prefix)); + let follow_label = ctx.block_label(follow_idx); let live_deref_idx = ctx.new_block(&format!("{}.deref.live", block_prefix)); let live_deref_label = ctx.block_label(live_deref_idx); - let live_handle = { + // A receiver that is not an Array leaves on its brand byte alone, before + // the forwarding, integrity, prototype and bounds work below that it would + // only fail. On the untyped `obj[i] = v` route (#10513) that is every + // typed-array and Buffer store, which declines onto the typed-array tier. + { let blk = ctx.block(); let arr_bits = blk.bitcast_double_to_i64(arr_box); let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); - let gc_type_addr = blk.sub(I64, &arr_handle, "8"); let gc_type_ptr = blk.inttoptr(I64, &gc_type_addr); let gc_type = blk.load(I8, &gc_type_ptr); let is_array = blk.icmp_eq(I8, &gc_type, "1"); // GC_TYPE_ARRAY + blk.cond_br(&is_array, &follow_label, &slow_label); + } + + ctx.current_block = follow_idx; + let live_handle = { + let blk = ctx.block(); + let arr_bits = blk.bitcast_double_to_i64(arr_box); + let arr_handle = blk.and(I64, &arr_bits, POINTER_MASK_I64); let gc_flags_addr = blk.sub(I64, &arr_handle, "7"); let gc_flags_ptr = blk.inttoptr(I64, &gc_flags_addr); @@ -137,8 +219,8 @@ pub(super) fn emit_guarded_inbounds_array_store( // Longer or corrupt chains still take the slow arm. let original_arr_ptr = blk.inttoptr(I64, &arr_handle); let forwarding_target = blk.load(I64, &original_arr_ptr); - let follow_forwarding = blk.and(I1, &is_array, &is_forwarded); - let live_handle = blk.select(I1, &follow_forwarding, I64, &forwarding_target, &arr_handle); + // `deref` branded the head as an Array. + let live_handle = blk.select(I1, &is_forwarded, I64, &forwarding_target, &arr_handle); let live_top = blk.lshr(I64, &live_handle, "48"); let live_top_clear = blk.icmp_eq(I64, &live_top, "0"); @@ -153,6 +235,13 @@ pub(super) fn emit_guarded_inbounds_array_store( }; ctx.current_block = live_deref_idx; + let idx_i32 = match index { + StoreIndex::I32(idx) => idx.to_string(), + StoreIndex::CanonicalOfDouble(idx_double) => { + emit_canonical_element_index_i32(ctx, idx_double) + } + }; + let idx_i32 = idx_i32.as_str(); let reserved = { let blk = ctx.block(); let arr_handle = live_handle.clone(); @@ -259,12 +348,21 @@ pub(super) fn emit_guarded_inbounds_array_store( // Decide that inline with the exact runtime predicate and call the // note only when it has work: the ECS `ents[id] = arch` store is a // pointer over a pointer into a proof-free array on every iteration. - let (value_bits, old_bits) = emit_jsvalue_slot_store_deferred_layout_note_on_block( - blk, - &element_ptr, - val_double, - ); - let new_is_pointer = emit_layout_pointer_bearing_check(blk, &value_bits); + let (value_bits, old_bits) = + emit_jsvalue_slot_store_deferred_layout_note_without_addref_on_block( + blk, + &element_ptr, + val_double, + ); + // `write_barrier_needed == false` is the caller's proof that the + // value carries non-pointer bits by construction + // (`array_store_needs_write_barrier`), so its classification is a + // constant and only the RETIRED value's needs testing. + let new_is_pointer = if write_barrier_needed { + emit_layout_pointer_bearing_check(blk, &value_bits) + } else { + "false".to_string() + }; let old_is_pointer = emit_layout_pointer_bearing_check(blk, &old_bits); let classification_changed = blk.icmp_ne(I1, &new_is_pointer, &old_is_pointer); let shape_bits = blk.and(I16, &reserved, "2048"); // GC_ARRAY_ELEMENT_SHAPE @@ -279,6 +377,13 @@ pub(super) fn emit_guarded_inbounds_array_store( ) } }; + if layout_note.is_some() && write_barrier_needed { + // The string demote the deferred store leaves to its caller, with the + // helper's own `STRING_TAG` test hoisted inline. A value with + // non-pointer bits by construction (no barrier needed) cannot be a + // heap string, so it needs neither the test nor the call. + super::helpers::emit_string_addref_if_heap_string(ctx, val_double); + } if let Some((old_bits, note_needed)) = layout_note { let note_idx = ctx.new_block(&format!("{}.laynote", block_prefix)); let note_done_idx = ctx.new_block(&format!("{}.laynote.done", block_prefix)); diff --git a/crates/perry-codegen/src/expr/index_set_typed_array.rs b/crates/perry-codegen/src/expr/index_set_typed_array.rs index 2409706012..9614801d0e 100644 --- a/crates/perry-codegen/src/expr/index_set_typed_array.rs +++ b/crates/perry-codegen/src/expr/index_set_typed_array.rs @@ -17,6 +17,8 @@ //! lowering is the caller's to close, and `index_set.rs`'s `#5525` arm is one //! of the sites #7640 records as still open. +use anyhow::Result; + use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8}; use super::FnCtx; @@ -45,14 +47,129 @@ pub(super) fn lower_inline_dyn_typed_array_set( idx_d: &str, val_double: &str, strict: bool, -) -> String { + array_arm: Option, +) -> Result { // As with the ordinary array store, an operand can throw before this // helper runs. Do not open fresh blocks using values dropped after the // terminator (#11450). No assignment value is consumed on this path. if ctx.block().is_terminated() { - return crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); + return Ok(crate::nanbox::double_literal(f64::from_bits( + crate::nanbox::TAG_UNDEFINED, + ))); } + let Some(facts) = array_arm else { + return Ok(emit_inline_ta_set_then_runtime( + ctx, obj_box, idx_d, val_double, strict, + )); + }; + // #10513: the ordinary-Array arm. An untyped `d[j] = v` onto a live plain + // Array used to reach `js_dyn_index_set_strict` on EVERY store, which + // re-classifies the receiver through the proxy / symbol / typed-array / + // buffer-registry / collection / prototype / arguments ladder before the + // array setter runs (~450 instructions per element on node-forge's jsbn + // `am1`). The guarded in-bounds store the statically-typed receivers + // already use (`index_set_guarded.rs`) proves everything that ladder + // would conclude for this case from the receiver's own header: a + // non-forwarded (or once-forwarded, healed inline) `GC_TYPE_ARRAY`, no + // frozen/sealed/non-extensible/descriptor bits, the default prototype + // chain, and a canonical index strictly below `length`. Every other + // receiver and key — a Buffer, a string or Symbol key, an append, a + // hole-creating sparse write — declines onto `js_dyn_index_set_strict`. + // + // The typed-array tier keeps its place in front: a receiver that hits the + // #5525 kind cache (the only way that tier's fast arm is reachable) goes + // there on one load and compare, so typed-array stores pay nothing for the + // Array arm, and an Array pays only that compare for the typed-array one. + let ta_idx = ctx.new_block("dynarr.ta"); + let array_idx = ctx.new_block("dynarr.array"); + let done_idx = ctx.new_block("dynarr.done"); + let ta_label = ctx.block_label(ta_idx); + let array_label = ctx.block_label(array_idx); + let done_label = ctx.block_label(done_idx); + { + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(obj_box); + let raw = blk.and(I64, &obj_bits, crate::nanbox::POINTER_MASK_I64); + let slot = blk.lshr(I64, &raw, "3"); + let slot = blk.and(I64, &slot, "63"); + let entry_ptr = blk.gep( + "[64 x i64]", + "@PERRY_TA_KIND_CACHE", + &[(I64, "0"), (I64, &slot)], + ); + let entry_val = blk.load(I64, &entry_ptr); + let entry_addr = blk.lshr(I64, &entry_val, "8"); + // A cache entry names a heap address, so a non-pointer box whose low + // 48 bits collide with one still fails the full guard in `dynarr.ta`. + let cached_typed_array = blk.icmp_eq(I64, &entry_addr, &raw); + blk.cond_br(&cached_typed_array, &ta_label, &array_label); + } + ctx.current_block = ta_idx; + let _ = emit_inline_ta_set_then_runtime(ctx, obj_box, idx_d, val_double, strict); + ctx.block().br(&done_label); + + ctx.current_block = array_idx; + super::index_set_guarded::emit_guarded_inbounds_array_store_keyed( + ctx, + obj_box, + super::index_set_guarded::StoreIndex::CanonicalOfDouble(idx_d), + val_double, + "dynarr.set", + facts.layout_note_needed, + facts.write_barrier_needed, + facts.value_is_numeric, + |ctx| { + emit_dyn_index_set_runtime(ctx, obj_box, idx_d, val_double, strict); + Ok(()) + }, + )?; + ctx.block().br(&done_label); + ctx.current_block = done_idx; + Ok(val_double.to_string()) +} +/// The complete dynamic `[[Set]]`, preserving the source function's +/// assignment strictness. +fn emit_dyn_index_set_runtime( + ctx: &mut FnCtx<'_>, + obj_box: &str, + idx_d: &str, + val_double: &str, + strict: bool, +) { + let strict = if strict { "1" } else { "0" }; + ctx.block().call( + DOUBLE, + "js_dyn_index_set_strict", + &[ + (DOUBLE, obj_box), + (DOUBLE, idx_d), + (DOUBLE, val_double), + (I32, strict), + ], + ); +} + +/// What the caller knows statically about an untyped `obj[i] = v` store's +/// VALUE, handed to the ordinary-Array arm of +/// [`lower_inline_dyn_typed_array_set`]. Same three facts, same predicates, as +/// every other caller of `emit_guarded_inbounds_array_store` passes. +#[derive(Clone, Copy, Debug)] +pub(super) struct DynArrayStoreFacts { + pub layout_note_needed: bool, + pub write_barrier_needed: bool, + pub value_is_numeric: bool, +} + +/// The #5525 guarded inline typed-array store, exiting to +/// `js_dyn_index_set_strict` on any guard miss. Returns the assignment's value. +fn emit_inline_ta_set_then_runtime( + ctx: &mut FnCtx<'_>, + obj_box: &str, + idx_d: &str, + val_double: &str, + strict: bool, +) -> String { let tag_mask = crate::nanbox::i64_literal(crate::nanbox::TAG_MASK); let pointer_tag = crate::nanbox::POINTER_TAG_I64; let pointer_mask = crate::nanbox::POINTER_MASK_I64; @@ -286,17 +403,7 @@ pub(super) fn lower_inline_dyn_typed_array_set( // ---- slow: preserve the source function's assignment strictness ---- ctx.current_block = slow_idx; - let strict = if strict { "1" } else { "0" }; - ctx.block().call( - DOUBLE, - "js_dyn_index_set_strict", - &[ - (DOUBLE, obj_box), - (DOUBLE, idx_d), - (DOUBLE, val_double), - (I32, strict), - ], - ); + emit_dyn_index_set_runtime(ctx, obj_box, idx_d, val_double, strict); ctx.block().br(&merge_label); // ---- merge: assignment yields the stored value on every path ---- diff --git a/crates/perry-codegen/src/expr/write_barrier.rs b/crates/perry-codegen/src/expr/write_barrier.rs index 2036188faf..6a0dbbe9aa 100644 --- a/crates/perry-codegen/src/expr/write_barrier.rs +++ b/crates/perry-codegen/src/expr/write_barrier.rs @@ -658,11 +658,13 @@ pub(crate) fn emit_jsvalue_slot_store_scalar_aware_on_block( /// The scalar-aware slot store with its layout note DEFERRED to the caller: /// loads the slot's previous value, writes the new one through the shared -/// (audited) store, runs the string-addref demote, and returns -/// `(value_bits, old_bits)` so the caller can decide inline whether the -/// runtime note would act at all before calling it. The caller owns both the -/// note and the barrier; nothing else about the store changes. -pub(crate) fn emit_jsvalue_slot_store_deferred_layout_note_on_block( +/// (audited) store, and returns `(value_bits, old_bits)` so the caller can +/// decide inline whether the runtime note would act at all before calling it. +/// The caller owns the note, the barrier AND the string-addref demote, which it +/// must emit (the guarded array store uses +/// `helpers::emit_string_addref_if_heap_string`, which tests `STRING_TAG` +/// inline so a Number never reaches the call). +pub(crate) fn emit_jsvalue_slot_store_deferred_layout_note_without_addref_on_block( blk: &mut LlBlock, slot_ptr: &str, value_double: &str, @@ -675,7 +677,7 @@ pub(crate) fn emit_jsvalue_slot_store_deferred_layout_note_on_block( value_double, "", "", - true, + false, false, "", "", diff --git a/crates/perry-codegen/src/stmt/loops.rs b/crates/perry-codegen/src/stmt/loops.rs index 73d3dbe6ed..0db54540b2 100644 --- a/crates/perry-codegen/src/stmt/loops.rs +++ b/crates/perry-codegen/src/stmt/loops.rs @@ -3016,6 +3016,72 @@ pub(super) fn packed_f64_range_loop_pure_expr_collect( /// Emit one range-guard call per accessed array (window endpoints merged /// from the counter part `[start + min_offset, bound + max_offset)` and the /// static part `[lo, hi]`), AND-reduced into a single i1. +/// #10514: heal a growth-forwarded receiver binding before the packed-range +/// loop guard judges it. The guard rejects a forwarding stub (the clone reads +/// `length` and the elements base straight off the binding), so an Array that +/// was grown by `a[i] = v` or `push` through ANOTHER reference — a parameter, +/// a field it was read from, a module global — sent every later loop over it +/// to the per-access slow clone for the rest of the program (~6x the +/// instructions per read). The repair is the element-shape preheader's +/// (#7480): follow the chain once with `js_array_refresh_local_head` and write +/// the live head back to the loop's own slot. It is gated inline on the +/// binding actually holding a forwarded `GC_TYPE_ARRAY`, so a live head pays +/// one header load and no call. Boxed bindings are skipped: their slot holds +/// the box, not the value (#11335). The guard re-loads the binding after this. +fn emit_packed_range_receiver_forwarding_repair(ctx: &mut FnCtx<'_>, array_id: u32) { + if ctx.boxed_vars.contains(&array_id) { + return; + } + let Some(slot) = ctx.locals.get(&array_id).cloned() else { + return; + }; + let Ok(arr0) = lower_expr(ctx, &perry_hir::Expr::LocalGet(array_id)) else { + return; + }; + let header_idx = ctx.new_block("packed_f64_range.fwd.header"); + let repair_idx = ctx.new_block("packed_f64_range.fwd.repair"); + let done_idx = ctx.new_block("packed_f64_range.fwd.done"); + let header_label = ctx.block_label(header_idx); + let repair_label = ctx.block_label(repair_idx); + let done_label = ctx.block_label(done_idx); + let handle = { + let blk = ctx.block(); + let bits = blk.bitcast_double_to_i64(&arr0); + let handle = blk.and(I64, &bits, crate::nanbox::POINTER_MASK_I64); + let tag = blk.lshr(I64, &bits, "48"); + let is_pointer = blk.icmp_eq(I64, &tag, crate::nanbox::POINTER_TAG_TOP16_I64); + let above_band = blk.icmp_ugt(I64, &handle, "1048575"); + let below_limit = blk.icmp_ult(I64, &handle, "140737488355328"); + let ok = blk.and(I1, &is_pointer, &above_band); + let ok = blk.and(I1, &ok, &below_limit); + blk.cond_br(&ok, &header_label, &done_label); + handle + }; + ctx.current_block = header_idx; + { + let blk = ctx.block(); + let type_addr = blk.sub(I64, &handle, "8"); + let type_ptr = blk.inttoptr(I64, &type_addr); + let gc_type = blk.load(I8, &type_ptr); + let is_array = blk.icmp_eq(I8, &gc_type, "1"); + let flags_addr = blk.sub(I64, &handle, "7"); + let flags_ptr = blk.inttoptr(I64, &flags_addr); + let flags = blk.load(I8, &flags_ptr); + let forwarded_bits = blk.and(I8, &flags, "128"); + let forwarded = blk.icmp_ne(I8, &forwarded_bits, "0"); + let stale = blk.and(I1, &is_array, &forwarded); + blk.cond_br(&stale, &repair_label, &done_label); + } + ctx.current_block = repair_idx; + { + let blk = ctx.block(); + let fresh = blk.call(DOUBLE, "js_array_refresh_local_head", &[(DOUBLE, &arr0)]); + blk.store(DOUBLE, &fresh, &slot); + blk.br(&done_label); + } + ctx.current_block = done_idx; +} + fn emit_packed_f64_range_guards( ctx: &mut FnCtx<'_>, matched: &PackedF64RangeLoop, @@ -3026,6 +3092,7 @@ fn emit_packed_f64_range_guards( let mut all_guards_ok: Option = None; let mut affine_window_proven: std::collections::BTreeSet = Default::default(); for access in &matched.arrays { + emit_packed_range_receiver_forwarding_repair(ctx, access.array_id); let arr_box = lower_expr(ctx, &perry_hir::Expr::LocalGet(access.array_id))?; let feedback_site_id = emit_typed_feedback_register_site( ctx, diff --git a/scripts/gc_store_site_inventory.py b/scripts/gc_store_site_inventory.py index a8bd3b059b..ef67be9da5 100644 --- a/scripts/gc_store_site_inventory.py +++ b/scripts/gc_store_site_inventory.py @@ -603,6 +603,7 @@ def scan_file(path: Path) -> list[Finding]: "emit_write_barrier_slot_value_and_generation_tested": 5, "emit_jsvalue_slot_store_pointer_tested": 11, "emit_guarded_inbounds_array_store": 4, + "emit_guarded_inbounds_array_store_keyed": 4, # The static-key store IC (`expr/put_value_store_ic.rs`): its bookkeeping # emitter takes the stem after the value. "emit_static_store_ic_bookkeeping": 7, @@ -611,7 +612,10 @@ def scan_file(path: Path) -> list[Finding]: # Emitter wrappers that forward a caller-supplied stem: their INTERNAL emitter # call passes an identifier, and the stem literal lives at THEIR call sites # (which the census scans through the same table above). -STEM_FORWARDERS = {"emit_guarded_inbounds_array_store"} +STEM_FORWARDERS = { + "emit_guarded_inbounds_array_store", + "emit_guarded_inbounds_array_store_keyed", +} STEM_REGISTRY_PATH = "crates/perry-codegen/src/expr/barrier_stem_census_tests.rs" STEM_REGISTRY_HEADER = "VERIFIED_BARRIER_STEMS" diff --git a/test-files/test_gap_10513_untyped_array_element_access.ts b/test-files/test_gap_10513_untyped_array_element_access.ts new file mode 100644 index 0000000000..a95e586877 --- /dev/null +++ b/test-files/test_gap_10513_untyped_array_element_access.ts @@ -0,0 +1,163 @@ +// #10513 / #10514: untyped (`any`) element stores and reads on ordinary +// Arrays take an inline guarded tier; everything the guard cannot prove must +// keep the full [[Set]]/[[Get]] semantics. Each block prints what it observed. + +// Module code is strict: a rejected store throws, so report it rather than stop. +function put(d: any, i: any, v: any): any { + try { return (d[i] = v); } catch (e: any) { console.log(" throw", String(i), e.constructor.name); return undefined; } +} +function get(d: any, i: any): any { return d[i]; } +function fill(d: any, n: number, k: number): void { for (let j = 0; j < n; j++) d[j] = j * k; } +function sum(d: any, n: number): number { let s = 0; for (let j = 0; j < n; j++) s += d[j]; return s; } +function sumTyped(d: number[], n: number): number { let s = 0; for (let j = 0; j < n; j++) s += d[j]; return s; } +function show(label: string, v: any): void { console.log(label, JSON.stringify(v)); } + +// 1. plain dense store/read, numbers and non-numbers +{ + const a: any = [1, 2, 3, 4]; + put(a, 0, 10); put(a, 1, "s"); put(a, 2, null); put(a, 3, { x: 1 }); + show("dense", a); + put(a, 1, 2.5); put(a, 3, -0); + console.log("dense2", a[1], Object.is(get(a, 3), -0)); +} + +// 2. holes: an in-range hole store, and reads of holes +{ + const a: any = [1, , 3]; + console.log("hole-read", get(a, 1), 1 in a); + put(a, 1, 7); + console.log("hole-store", get(a, 1), 1 in a, a.length); + const b: any = new Array(5); + put(b, 3, "x"); + console.log("presized-holes", JSON.stringify(b), 0 in b, 3 in b); +} + +// 3. length growth, append, sparse store past the end +{ + const a: any = []; + for (let i = 0; i < 40; i++) put(a, i, i * 2); + console.log("grown", a.length, sum(a, 40), sumTyped(a, 40)); + put(a, 45, 1); + console.log("sparse", a.length, get(a, 44), 44 in a, get(a, 45)); + const holder: any = { data: [] }; + for (let i = 0; i < 100; i++) holder.data[i] = i; // grows through a field + console.log("grown-field", holder.data.length, sum(holder.data, 100), get(holder.data, 99)); + const pushed: any = []; + for (let i = 0; i < 64; i++) pushed.push(i + 0.5); + console.log("pushed", sum(pushed, 64), sumTyped(pushed, 64)); + fill(pushed, 64, 3); + console.log("refill", sum(pushed, 64), get(pushed, 63)); +} + +// 4. out-of-bounds and non-canonical keys +{ + const a: any = [1, 2, 3]; + console.log("oob-read", get(a, 3), get(a, -1), get(a, 1.5), get(a, NaN), get(a, 4294967295)); + put(a, -1, "neg"); put(a, 1.5, "frac"); put(a, "2", "str2"); put(a, "k", "named"); + console.log("keys", a.length, JSON.stringify(Object.keys(a)), a[-1], a[1.5], a[2], a.k); + const sym = Symbol("s"); + put(a, sym, "symval"); + console.log("symbol", a[sym], a.length); + put(a, 4294967294, "max"); + console.log("maxidx", a.length); +} + +// 5. frozen / sealed / non-extensible arrays (strict: rejected stores throw) +{ + const f: any = Object.freeze([1, 2, 3]); + put(f, 0, 99); put(f, 3, 99); + console.log("frozen", JSON.stringify(f)); + const s: any = Object.seal([1, 2, 3]); + put(s, 0, 99); put(s, 3, 99); + console.log("sealed", JSON.stringify(s)); + const n: any = Object.preventExtensions([1, 2, 3]); + put(n, 1, 42); put(n, 5, 42); + console.log("noext", JSON.stringify(n), n.length); +} + +// 6. arrays carrying named props, accessors and descriptors +{ + const a: any = [1, 2, 3]; + a.tag = "t"; + put(a, 1, 20); + console.log("named", JSON.stringify(a), a.tag); + const log: string[] = []; + Object.defineProperty(a, 0, { get() { log.push("get0"); return 7; }, set(v) { log.push("set0=" + v); }, configurable: true }); + put(a, 0, 11); const r = get(a, 0); + console.log("accessor", r, log.join(",")); + Object.defineProperty(a, 2, { value: 3, writable: false }); + put(a, 2, 30); + console.log("readonly", get(a, 2)); +} + +// 7. Object.setPrototypeOf on an array, and an indexed setter on the chain +{ + const log: string[] = []; + const proto = Object.create(Array.prototype); + Object.defineProperty(proto, 1, { set(v) { log.push("protoset=" + v); }, get() { return "p1"; }, configurable: true }); + const a: any = [0, , 2]; + Object.setPrototypeOf(a, proto); + put(a, 1, 5); // hole: goes to the inherited setter + put(a, 0, 9); // own element + console.log("setproto", get(a, 0), get(a, 1), 1 in a, Object.hasOwn(a, 1), log.join(",")); + const b: any = [1, 2]; + Object.setPrototypeOf(b, null); + put(b, 0, "np"); put(b, 5, "np5"); + console.log("nullproto", b[0], b[5], b.length); +} + +// 8. Array.prototype pollution with an indexed accessor +{ + const log: string[] = []; + Object.defineProperty(Array.prototype, 3, { set(v) { log.push("AP3=" + v); }, get() { return "ap3"; }, configurable: true }); + const a: any = [0, 1, 2]; + put(a, 3, "x"); // append position: inherited setter wins + const h: any = [0, 1, 2, , 4]; + put(h, 3, "y"); // hole: inherited setter wins + console.log("ap-pollute", a.length, get(a, 3), get(h, 3), log.join(",")); + delete (Array.prototype as any)[3]; + put(h, 3, "z"); + console.log("ap-clean", get(h, 3)); +} + +// 9. typed arrays, Buffers and shared buffers through the same untyped sites +{ + const u16: any = new Uint16Array(4); const f64: any = new Float64Array(3); const c: any = new Uint8ClampedArray(2); + put(u16, 0, 70000); put(u16, 1, -1); put(u16, 2, 3.9); put(u16, 9, 1); + put(f64, 0, 1.25); put(f64, 1, "2.5"); put(f64, 2, true); + put(c, 0, 300); put(c, 1, 1.5); + console.log("typed", Array.from(u16).join(","), Array.from(f64).join(","), Array.from(c).join(",")); + const sab = new SharedArrayBuffer(8); + const v1: any = new Int32Array(sab); const v2: any = new Int32Array(sab, 4, 1); + put(v1, 1, 123); put(v2, 0, get(v2, 0) + 1); + console.log("shared", get(v1, 1), get(v2, 0)); + const ab = new ArrayBuffer(8); const v3: any = new Uint8Array(ab, 2, 4); const v4: any = new Uint8Array(ab); + put(v3, 0, 255); put(v4, 3, 7); + console.log("views", Array.from(v4).join(","), get(v3, 1)); + const buf: any = Buffer.alloc(4); + put(buf, 0, 257); put(buf, 1, 65); put(buf, 9, 1); + console.log("buffer", buf.toString("hex"), get(buf, 1), get(buf, 9)); +} + +// 10. primitive receivers (reads only: a strict store onto a primitive is a +// separate, pre-existing gap) +{ + const s: any = "abc"; + const n: any = 5; + console.log("string", get(s, 1), get(s, 5), get(n, 0)); +} + +// 11. arguments objects keep their mapped semantics +{ + function f(a: any, b: any) { put(arguments, 0, "A"); put(arguments, 1, "B"); return a + "|" + b + "|" + get(arguments, 0); } + console.log("arguments", f(1, 2)); +} + +// 12. read-modify-write and many writes through a grown, re-read binding +{ + const g: any = []; + for (let i = 0; i < 200; i++) g[i] = i; + const ref: any = { data: g }; + for (let r = 0; r < 3; r++) for (let i = 0; i < 200; i++) ref.data[i] = ref.data[i] + 1; + console.log("rmw", sum(ref.data, 200), sumTyped(g, 200), g === ref.data); +}