@@ -66,7 +66,8 @@ use crate::types::{DOUBLE, I1, I16, I32, I64, I8};
6666use super :: {
6767 array_store_needs_layout_note, array_store_needs_write_barrier,
6868 emit_array_numeric_write_note_on_block, emit_jsvalue_slot_store_with_flags_on_block,
69- emit_jsvalue_slot_store_with_value_bits_on_block, emit_root_nanbox_store_on_block,
69+ emit_jsvalue_slot_store_with_value_bits_on_block, emit_layout_note_slot_on_block,
70+ emit_may_carry_heap_pointer_check, emit_root_nanbox_store_on_block,
7071 emit_typed_feedback_register_site, emit_write_barrier,
7172 emit_write_barrier_slot_generation_tested, expr_has_numeric_pointer_free_array_layout,
7273 lower_expr, lower_expr_native, nanbox_pointer_inline, raw_f64_layout_fact, unbox_to_i64, FnCtx ,
@@ -84,6 +85,142 @@ use super::{
8485/// expression, never an operand — a consumed `n = arr.push(x)` always
8586/// computes the real length). When set, the placeholder constant is returned
8687/// without emitting the call.
88+ /// The `nofwd` admission test for a #7830 numeric push: the historical
89+ /// integrity mask `0x0407` PLUS the three `_reserved` states in which
90+ /// `js_gc_note_slot_layout` does real work for a **non-pointer** value stored
91+ /// into a `GC_TYPE_ARRAY`. Every other state that function can be in is a
92+ /// provable no-op for such a value (see
93+ /// [`emit_numeric_push_store_pointer_tested`]).
94+ ///
95+ /// * `0x0407` `FROZEN|SEALED|NO_EXTEND|ARRAY_DESCRIPTORS` — the historical
96+ /// integrity bits, unchanged in meaning and in destination.
97+ /// * `0x0800` `GC_ARRAY_ELEMENT_SHAPE` — a live element-shape proof (#7480).
98+ /// `note_element_store` must CLEAR it when a non-object lands in the array,
99+ /// and that call sits ahead of every early return in `layout_note_slot`.
100+ /// * `0x1000` — `GC_OBJ_TYPED_LAYOUT_INTACT` as `layout_note_slot` reads it
101+ /// (`GC_ARRAY_RAW_F64_HOLES` as `gc::types` writes it for an array; the two
102+ /// share the bit and are disjoint by `obj_type`). Set, it routes into the
103+ /// typed-descriptor probe, whose `slot_index >= slot_count` arm downgrades.
104+ /// * `0x2000` `GC_LAYOUT_ALL_POINTERS` — a non-pointer store into an
105+ /// all-pointer array calls `layout_mark_unknown`, which is a real state
106+ /// change, not a no-op.
107+ ///
108+ /// `GC_LAYOUT_SIDE_MASK` is deliberately absent. Skipping the note there leaves
109+ /// a stale set bit over a non-pointer, and `mark_field_into_worklist`
110+ /// re-validates every slot word, so the cost is one rejected visit and never a
111+ /// stranded child — the identical argument `class_field_store_needs_layout_note`
112+ /// already ships.
113+ ///
114+ /// Failing this test costs the push its inline store: it takes
115+ /// `js_array_push_f64`, which notes the slot exactly as it always did. So a
116+ /// widening here can only ever be slower, never wrong — the same direction of
117+ /// approximation `emit_may_carry_heap_pointer_check` documents.
118+ ///
119+ /// `0x0407 | 0x3800` == `0x3C07` == 15367.
120+ const ARRAY_PUSH_NUMERIC_CLEAN_I16 : & str = "15367" ;
121+
122+ /// #7830 — the inline array append's GC bookkeeping behind ONE live test.
123+ ///
124+ /// The `apush.inbounds` store used to pay `js_string_addref_if_heap_string` +
125+ /// `js_gc_note_slot_layout` unconditionally and then an `ldar` on
126+ /// `PERRY_INCREMENTAL_MARK_BARRIER_ACTIVE_COUNT` for the barrier gate — three
127+ /// cross-crate obligations on *every* element of a `number[]` push loop, where
128+ /// all three are dead. `bench/push_num.ts` is 20M such pushes.
129+ ///
130+ /// The static proof that would retire them (`array_store_needs_layout_note` →
131+ /// `expr_produces_non_pointer_bits_by_construction`) cannot be made for the
132+ /// shape that matters: `keep.push(base + j)` is an `Expr::Binary { Add }`, and
133+ /// that arm answers `false` unconditionally because `+` is string concatenation
134+ /// for non-numeric operands. It fires only for a bare canonical-i32 local
135+ /// (`keep.push(j)`), which is why the same loop is ~1.7x faster written that
136+ /// way. This is #7511's answer to the identical problem on class fields: ask
137+ /// the question ONCE inline, on the live bits, and branch over all three.
138+ ///
139+ /// Why each obligation is dead when the test says no:
140+ ///
141+ /// * `js_string_addref_if_heap_string` is tag-checked and a no-op for every
142+ /// non-`STRING_TAG` value — `emit_may_carry_heap_pointer_check` admits
143+ /// `STRING_TAG`, so a string always takes the guarded arm.
144+ /// * `js_write_barrier_slot` opens with `barrier_child_prologue`, which returns
145+ /// immediately when `decode_heap_addr(child) == 0`. The predicate is a
146+ /// superset of every address that decoder resolves.
147+ /// * `js_gc_note_slot_layout` for a non-pointer value is a no-op in every
148+ /// layout state EXCEPT three, and reaching this block already PROVES all
149+ /// three clear: [`ARRAY_PUSH_NUMERIC_CLEAN_I16`] widens the `nofwd`
150+ /// integrity mask to cover them, so the array's half of the proof costs a
151+ /// wider constant on an `and` that was being emitted anyway, and this block
152+ /// has only the value left to test.
153+ ///
154+ /// Gated on `value_is_numeric` at the call site, so a pointer-pushing loop
155+ /// (`churn`, `tree`, `push_cls`) emits byte-identical IR to before rather than
156+ /// paying the predicate for a test it always fails. That also keeps
157+ /// `js_array_note_numeric_write` out of the picture: it is already statically
158+ /// elided for exactly this class of value.
159+ #[ allow( clippy:: too_many_arguments) ]
160+ fn emit_numeric_push_store_pointer_tested (
161+ ctx : & mut FnCtx < ' _ > ,
162+ arr_handle : & str ,
163+ value_double : & str ,
164+ value_bits_override : Option < & str > ,
165+ string_addref_needed : bool ,
166+ layout_note_needed : bool ,
167+ write_barrier_needed : bool ,
168+ ) -> ( String , String , Option < String > ) {
169+ let ( length, element_addr, value_bits) = {
170+ let blk = ctx. block ( ) ;
171+ let length = blk. safe_load_i32_from_ptr ( arr_handle) ;
172+ let length_i64 = blk. zext ( I32 , & length, I64 ) ;
173+ let byte_offset = blk. shl ( I64 , & length_i64, "3" ) ;
174+ let with_header = blk. add ( I64 , & byte_offset, "8" ) ;
175+ let element_addr = blk. add ( I64 , arr_handle, & with_header) ;
176+ let element_ptr = blk. inttoptr ( I64 , & element_addr) ;
177+ // GC_STORE_AUDIT(BARRIERED): the slot write itself is unconditional;
178+ // only the bookkeeping moves behind the live test below, and the
179+ // barrier's own first test is a subset of that predicate.
180+ blk. store ( DOUBLE , value_double, & element_ptr) ;
181+ let value_bits = value_bits_override
182+ . map ( ToOwned :: to_owned)
183+ . unwrap_or_else ( || blk. bitcast_double_to_i64 ( value_double) ) ;
184+ ( length, element_addr, value_bits)
185+ } ;
186+ let bookkeeping_idx = ctx. new_block ( "apush.gc_bookkeeping" ) ;
187+ let done_idx = ctx. new_block ( "apush.gc_bookkeeping.done" ) ;
188+ let bookkeeping_label = ctx. block_label ( bookkeeping_idx) ;
189+ let done_label = ctx. block_label ( done_idx) ;
190+ {
191+ let blk = ctx. block ( ) ;
192+ let may_carry_pointer = emit_may_carry_heap_pointer_check ( blk, & value_bits) ;
193+ blk. cond_br ( & may_carry_pointer, & bookkeeping_label, & done_label) ;
194+ }
195+ ctx. current_block = bookkeeping_idx;
196+ {
197+ let blk = ctx. block ( ) ;
198+ if string_addref_needed {
199+ blk. call_void ( "js_string_addref_if_heap_string" , & [ ( DOUBLE , value_double) ] ) ;
200+ }
201+ if layout_note_needed {
202+ emit_layout_note_slot_on_block ( blk, arr_handle, & length, & value_bits) ;
203+ }
204+ }
205+ if write_barrier_needed {
206+ // `arr_handle` reached here through the `nofwd` header test, so it is a
207+ // live, non-forwarded GC array user pointer — the precondition for
208+ // reading its header byte. The generation test stays: this arm is
209+ // reached for real pointer children too.
210+ emit_write_barrier_slot_generation_tested (
211+ ctx,
212+ arr_handle,
213+ arr_handle,
214+ & element_addr,
215+ & value_bits,
216+ "apush" ,
217+ ) ;
218+ }
219+ ctx. block ( ) . br ( & done_label) ;
220+ ctx. current_block = done_idx;
221+ ( length, element_addr, None )
222+ }
223+
87224fn emit_array_handle_length (
88225 ctx : & mut FnCtx < ' _ > ,
89226 array_handle : & str ,
@@ -423,6 +560,14 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) ->
423560 let value_is_numeric = is_numeric_expr ( ctx, value) ;
424561 let require_numeric_layout =
425562 value_is_numeric && expr_has_numeric_pointer_free_array_layout ( ctx, & array_expr) ;
563+ // #7830 — the inline append's three GC-bookkeeping calls behind ONE
564+ // live test of the stored bits, exactly #7511's class-field shape.
565+ // See `emit_numeric_push_store_pointer_tested` for why each call is
566+ // dead when the test says "no pointer, no watched layout state",
567+ // and why the gate is `value_is_numeric` rather than unconditional.
568+ let guarded_numeric_bookkeeping = value_is_numeric
569+ && !declared_all_pointer
570+ && ( layout_note_needed || string_addref_needed || write_barrier_needed) ;
426571 // #7634: spec order (receiver Reference, then argument) is only
427572 // observable when the argument can rebind the receiver. When it
428573 // can, take the rooted spec-ordered arm; when it cannot — the hot
@@ -764,6 +909,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) ->
764909 // notes the slot); it can never cost correctness.
765910 let admitted_bits = blk. and ( I16 , & obj_flags, "62599" ) ;
766911 blk. icmp_eq ( I16 , & admitted_bits, "40960" )
912+ } else if guarded_numeric_bookkeeping {
913+ // #7830 — the array's half of the guard, folded into the
914+ // integrity test rather than emitted as a second one:
915+ // same `and`, same `icmp`, a wider constant. Reaching
916+ // the inline store now additionally proves the three
917+ // `_reserved` states in which `js_gc_note_slot_layout`
918+ // does real work for a NON-pointer value, so the store's
919+ // guard has only the value left to test. See
920+ // `emit_numeric_push_store_pointer_tested`.
921+ let admitted_bits = blk. and ( I16 , & obj_flags, ARRAY_PUSH_NUMERIC_CLEAN_I16 ) ;
922+ blk. icmp_eq ( I16 , & admitted_bits, "0" )
767923 } else {
768924 // FROZEN(0x1)|SEALED(0x2)|NO_EXTEND(0x4)|ARRAY_DESCRIPTORS(0x400).
769925 let integrity_bits = blk. and ( I16 , & obj_flags, "1031" ) ;
@@ -796,7 +952,17 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr, value_discarded: bool) ->
796952 // between the store and the barrier would run with the
797953 // old→young edge unrecorded. The block is split here rather
798954 // than the call being sunk to the end of the block.
799- let ( length, element_addr, barrier_value_bits) = {
955+ let ( length, element_addr, barrier_value_bits) = if guarded_numeric_bookkeeping {
956+ emit_numeric_push_store_pointer_tested (
957+ ctx,
958+ & arr_handle,
959+ & v,
960+ v_bits. as_deref ( ) ,
961+ string_addref_needed,
962+ layout_note_needed,
963+ write_barrier_needed,
964+ )
965+ } else {
800966 let blk = ctx. block ( ) ;
801967 let length = blk. safe_load_i32_from_ptr ( & arr_handle) ;
802968 let length_i64 = blk. zext ( I32 , & length, I64 ) ;
0 commit comments