@@ -1365,7 +1365,7 @@ unsafe fn map_set_string_key_value(
13651365 let size = ( * map) . size ;
13661366 let entries = entries_ptr_mut ( map) ;
13671367 if grew && size > 0 {
1368- crate :: gc:: runtime_dirty_external_slot_span (
1368+ crate :: gc:: runtime_write_barrier_external_slot_span (
13691369 map as usize ,
13701370 entries as usize ,
13711371 size as usize * 2 ,
@@ -1474,7 +1474,7 @@ fn map_set_resolved(map: *mut MapHeader, key: f64, value: f64) {
14741474 let size = ( * map) . size ;
14751475 let entries = entries_ptr_mut ( map) ;
14761476 if grew && size > 0 {
1477- crate :: gc:: runtime_dirty_external_slot_span (
1477+ crate :: gc:: runtime_write_barrier_external_slot_span (
14781478 map as usize ,
14791479 entries as usize ,
14801480 size as usize * 2 ,
@@ -1843,79 +1843,92 @@ unsafe fn delete_entry_at_index(map: *mut MapHeader, idx: i32) -> i32 {
18431843 return 0 ;
18441844 }
18451845 let entries = entries_ptr_mut ( map) ;
1846+ let deleted_key = ptr:: read ( entries. add ( idx * 2 ) ) ;
18461847
18471848 // #2831: preserve insertion order. JS Map iteration must keep the
18481849 // relative order of surviving entries after a delete (and a
18491850 // delete-then-re-add appends at the end). The previous swap-and-pop
1850- // moved the last entry into the hole, reordering iteration. Shift
1851- // every entry after `idx` down by one slot instead.
1852- for i in idx..( size as usize - 1 ) {
1853- let next_key = ptr:: read ( entries. add ( ( i + 1 ) * 2 ) ) ;
1854- let next_value = ptr:: read ( entries. add ( ( i + 1 ) * 2 + 1 ) ) ;
1855- // GC_STORE_AUDIT(EXTERNAL_BARRIERED): map compaction slots use the shared external-slot helper.
1856- crate :: gc:: runtime_store_external_jsvalue_slot (
1857- map as usize ,
1858- entries. add ( i * 2 ) as usize ,
1859- next_key. to_bits ( ) ,
1851+ // moved the last entry into the hole, reordering iteration. Compact the
1852+ // already-owned key/value pairs with one overlap-safe move. This does not
1853+ // create a new parent -> child edge: every copied value was already in
1854+ // this Map. The span mark preserves the old -> young remembered-set
1855+ // contract for the slots' new addresses without paying two full runtime
1856+ // stores per entry.
1857+ let moved_entries = size as usize - idx - 1 ;
1858+ if moved_entries > 0 {
1859+ // GC_STORE_AUDIT(EXTERNAL_BARRIERED): ordered compaction is followed by a dirty-span barrier for every moved slot.
1860+ ptr:: copy (
1861+ entries. add ( ( idx + 1 ) * 2 ) ,
1862+ entries. add ( idx * 2 ) ,
1863+ moved_entries * 2 ,
18601864 ) ;
1861- crate :: gc:: runtime_store_external_jsvalue_slot (
1865+ crate :: gc:: runtime_write_barrier_external_slot_span (
18621866 map as usize ,
1863- entries. add ( i * 2 + 1 ) as usize ,
1864- next_value . to_bits ( ) ,
1867+ entries. add ( idx * 2 ) as usize ,
1868+ moved_entries * 2 ,
18651869 ) ;
18661870 }
18671871
18681872 ( * map) . size = size - 1 ;
18691873
1870- // The shift changes the entry index of every surviving key at or
1871- // after `idx`, so the O(1) lookup side-tables can't be patched in
1872- // place cheaply. Rebuild them from the compacted buffer.
1873- rebuild_map_index ( map) ;
1874+ // The old implementation rebuilt all three indexes from the entries
1875+ // buffer after every ordered delete. Repair their existing u32 offsets
1876+ // in place instead: removing one key and decrementing later offsets is a
1877+ // cache-linear pass over index values and does not re-hash surviving keys.
1878+ repair_map_indices_after_ordered_delete ( map, deleted_key, idx as u32 ) ;
18741879 1
18751880}
18761881
1877- /// Rebuild the numeric + string lookup side-tables for `map` from its
1878- /// current compacted entries buffer. Used after an order-preserving
1879- /// `delete` shifts entry indexes (#2831).
1880- unsafe fn rebuild_map_index ( map : * mut MapHeader ) {
1881- if map. is_null ( ) {
1882- return ;
1883- }
1884- let size = ( * map) . size as usize ;
1885- let capacity = ( * map) . capacity as usize ;
1886- if size > capacity || size > 16_000_000 || ( * map) . entries . is_null ( ) {
1887- return ;
1888- }
1889- let entries = entries_ptr ( map) ;
1890- MAP_INDEX . with ( |idx| {
1891- let mut idx = idx. borrow_mut ( ) ;
1892- let slot = idx
1893- . entry ( map as usize )
1894- . or_insert_with ( crate :: fast_hash:: new_ptr_hash_map) ;
1895- slot. clear ( ) ;
1896- for i in 0 ..size {
1897- let key_bits = ptr:: read ( entries. add ( i * 2 ) ) . to_bits ( ) ;
1898- if is_safe_numeric_key ( key_bits) {
1899- slot. insert ( NumericKey ( key_bits) , i as u32 ) ;
1882+ unsafe fn repair_map_indices_after_ordered_delete (
1883+ map : * mut MapHeader ,
1884+ deleted_key : f64 ,
1885+ deleted_idx : u32 ,
1886+ ) {
1887+ let map_addr = map as usize ;
1888+ let deleted_bits = deleted_key. to_bits ( ) ;
1889+
1890+ MAP_INDEX . with ( |indexes| {
1891+ let mut indexes = indexes. borrow_mut ( ) ;
1892+ if let Some ( index) = indexes. get_mut ( & map_addr) {
1893+ if is_safe_numeric_key ( deleted_bits) {
1894+ index. remove ( & NumericKey ( deleted_bits) ) ;
1895+ }
1896+ for entry_idx in index. values_mut ( ) {
1897+ if * entry_idx > deleted_idx {
1898+ * entry_idx -= 1 ;
1899+ }
19001900 }
19011901 }
19021902 } ) ;
1903- MAP_STRING_INDEX . with ( |idx| {
1904- let mut idx = idx. borrow_mut ( ) ;
1905- let slot = idx
1906- . entry ( map as usize )
1907- . or_insert_with ( std:: collections:: HashMap :: new) ;
1908- slot. clear ( ) ;
1909- for i in 0 ..size {
1910- let key_bits = ptr:: read ( entries. add ( i * 2 ) ) . to_bits ( ) ;
1911- if is_string_like ( key_bits) {
1912- if let Some ( h) = string_content_hash ( key_bits) {
1913- slot. entry ( h) . or_insert_with ( Vec :: new) . push ( i as u32 ) ;
1903+
1904+ MAP_STRING_INDEX . with ( |indexes| {
1905+ let mut indexes = indexes. borrow_mut ( ) ;
1906+ if let Some ( index) = indexes. get_mut ( & map_addr) {
1907+ for bucket in index. values_mut ( ) {
1908+ bucket. retain ( |entry_idx| * entry_idx != deleted_idx) ;
1909+ for entry_idx in bucket {
1910+ if * entry_idx > deleted_idx {
1911+ * entry_idx -= 1 ;
1912+ }
1913+ }
1914+ }
1915+ index. retain ( |_, bucket| !bucket. is_empty ( ) ) ;
1916+ }
1917+ } ) ;
1918+
1919+ MAP_PTR_INDEX . with ( |indexes| {
1920+ let mut indexes = indexes. borrow_mut ( ) ;
1921+ if let Some ( index) = indexes. get_mut ( & map_addr) {
1922+ if is_ptr_index_key ( deleted_bits) {
1923+ index. remove ( & MapPtrKey ( deleted_key) ) ;
1924+ }
1925+ for entry_idx in index. values_mut ( ) {
1926+ if * entry_idx > deleted_idx {
1927+ * entry_idx -= 1 ;
19141928 }
19151929 }
19161930 }
19171931 } ) ;
1918- rebuild_map_ptr_index ( map) ;
19191932}
19201933
19211934/// Rebuild ONLY the pointer-key index for `map` from its current entries
@@ -2687,4 +2700,108 @@ mod tests {
26872700 assert_eq ! ( js_map_delete_number_key( map, boxed_string_key) , 1 ) ;
26882701 assert_eq ! ( js_map_has( map, boxed_string_key) , 0 ) ;
26892702 }
2703+
2704+ #[ test]
2705+ fn ordered_delete_repairs_mixed_side_indexes_and_preserves_order ( ) {
2706+ let map = js_map_alloc ( 32 ) ;
2707+ let string_keys = ( 0 ..12 )
2708+ . map ( |i| {
2709+ let bytes = format ! ( "key-{i:02}" ) . into_bytes ( ) ;
2710+ js_string_from_bytes ( bytes. as_ptr ( ) , bytes. len ( ) as u32 )
2711+ } )
2712+ . collect :: < Vec < _ > > ( ) ;
2713+
2714+ for ( i, string_key) in string_keys. iter ( ) . copied ( ) . enumerate ( ) {
2715+ js_map_set ( map, i as f64 , ( i * 10 ) as f64 ) ;
2716+ js_map_set_string_number ( map, string_key, ( i * 10 + 1 ) as f64 ) ;
2717+ }
2718+ // Keep the backing allocations alive while using their tagged
2719+ // addresses as identity keys. They deliberately are not GC objects:
2720+ // this exercises the pointer-key index without introducing an
2721+ // allocation/collection point into the ordered-delete fixture.
2722+ let pointer_owners = ( 0 ..4 ) . map ( Box :: new) . collect :: < Vec < _ > > ( ) ;
2723+ let pointer_keys = pointer_owners
2724+ . iter ( )
2725+ . map ( |owner| {
2726+ f64:: from_bits (
2727+ crate :: value:: POINTER_TAG
2728+ | ( ( owner. as_ref ( ) as * const i32 as u64 ) & crate :: value:: POINTER_MASK ) ,
2729+ )
2730+ } )
2731+ . collect :: < Vec < _ > > ( ) ;
2732+ for ( i, key) in pointer_keys. iter ( ) . copied ( ) . enumerate ( ) {
2733+ js_map_set ( map, key, ( 1_000 + i) as f64 ) ;
2734+ }
2735+ assert_eq ! ( js_map_size( map) , 28 ) ;
2736+
2737+ assert_eq ! ( js_map_delete_number_key( map, 2.0 ) , 1 ) ;
2738+ assert_eq ! ( js_map_delete_string_key( map, string_keys[ 4 ] ) , 1 ) ;
2739+ assert_eq ! ( js_map_delete( map, pointer_keys[ 1 ] ) , 1 ) ;
2740+ assert_eq ! ( js_map_size( map) , 25 ) ;
2741+ assert_eq ! ( js_map_has_number_key( map, 2.0 ) , 0 ) ;
2742+ assert_eq ! ( js_map_has_string_key( map, string_keys[ 4 ] ) , 0 ) ;
2743+ assert_eq ! ( js_map_has( map, pointer_keys[ 1 ] ) , 0 ) ;
2744+
2745+ for ( i, string_key) in string_keys. iter ( ) . copied ( ) . enumerate ( ) {
2746+ if i != 2 {
2747+ assert_eq ! ( js_map_get_number_key( map, i as f64 ) , ( i * 10 ) as f64 ) ;
2748+ assert ! ( test_map_numeric_index_contains( map, i as f64 ) ) ;
2749+ }
2750+ if i != 4 {
2751+ assert_eq ! ( js_map_get_string_key( map, string_key) , ( i * 10 + 1 ) as f64 ) ;
2752+ assert ! ( test_map_string_index_contains(
2753+ map,
2754+ boxed_heap_string_key( string_key)
2755+ ) ) ;
2756+ }
2757+ }
2758+ for ( i, key) in pointer_keys. iter ( ) . copied ( ) . enumerate ( ) {
2759+ if i != 1 {
2760+ assert_eq ! ( js_map_get( map, key) , ( 1_000 + i) as f64 ) ;
2761+ assert ! ( test_map_ptr_index_contains( map, key) ) ;
2762+ }
2763+ }
2764+
2765+ let mut expected_keys = ( 0 ..12 )
2766+ . flat_map ( |i| {
2767+ let mut keys = Vec :: new ( ) ;
2768+ if i != 2 {
2769+ keys. push ( ( i as f64 ) . to_bits ( ) ) ;
2770+ }
2771+ if i != 4 {
2772+ keys. push ( boxed_heap_string_key ( string_keys[ i] ) . to_bits ( ) ) ;
2773+ }
2774+ keys
2775+ } )
2776+ . collect :: < Vec < _ > > ( ) ;
2777+ expected_keys. extend (
2778+ pointer_keys
2779+ . iter ( )
2780+ . enumerate ( )
2781+ . filter ( |( i, _) | * i != 1 )
2782+ . map ( |( _, key) | key. to_bits ( ) ) ,
2783+ ) ;
2784+ let actual_keys = ( 0 ..js_map_size ( map) )
2785+ . map ( |i| js_map_entry_key_at ( map, i) . to_bits ( ) )
2786+ . collect :: < Vec < _ > > ( ) ;
2787+ assert_eq ! (
2788+ actual_keys, expected_keys,
2789+ "delete must preserve survivor order"
2790+ ) ;
2791+
2792+ js_map_set_number_key ( map, 2.0 , 222.0 ) ;
2793+ js_map_set_string_number ( map, string_keys[ 4 ] , 444.0 ) ;
2794+ js_map_set ( map, pointer_keys[ 1 ] , 1_111.0 ) ;
2795+ assert_eq ! ( js_map_size( map) , 28 ) ;
2796+ assert_eq ! ( js_map_entry_key_at( map, 25 ) . to_bits( ) , 2.0f64 . to_bits( ) ) ;
2797+ assert_eq ! (
2798+ js_map_entry_key_at( map, 26 ) . to_bits( ) ,
2799+ boxed_heap_string_key( string_keys[ 4 ] ) . to_bits( ) ,
2800+ "delete-then-re-add must append at the end"
2801+ ) ;
2802+ assert_eq ! (
2803+ js_map_entry_key_at( map, 27 ) . to_bits( ) ,
2804+ pointer_keys[ 1 ] . to_bits( )
2805+ ) ;
2806+ }
26902807}
0 commit comments