@@ -1787,197 +1787,41 @@ fn is_internal_property_key(key: &str) -> bool {
17871787 key. starts_with ( '\0' )
17881788}
17891789
1790+ /// Parses an array index in its canonical decimal spelling.
1791+ ///
1792+ /// This is a predicate on the element path -- every string-keyed array access
1793+ /// asks it, and `has_index`, the element read, and the element write each ask
1794+ /// it again -- and it used to answer by formatting the parsed number back into
1795+ /// a fresh `String` to compare against the key. That is a heap allocation per
1796+ /// array access, and it showed up as such: 39 allocator samples on
1797+ /// `string-tagcloud` alone.
1798+ ///
1799+ /// The canonical form is exactly what the digits already say: no sign, no
1800+ /// leading zero unless the key is `"0"`, and a value below `u32::MAX` (the
1801+ /// array length itself is not an index).
17901802fn array_index_property_key ( key : & str ) -> Option < u32 > {
1791- key. parse :: < u32 > ( )
1792- . ok ( )
1793- . filter ( |index| * index < u32:: MAX && index. to_string ( ) == key)
1803+ let bytes = key. as_bytes ( ) ;
1804+ // `u32::MAX` has ten digits, so anything longer is out of range; a leading
1805+ // zero is a distinct string property rather than an index.
1806+ if bytes. is_empty ( ) || bytes. len ( ) > 10 || ( bytes[ 0 ] == b'0' && bytes. len ( ) > 1 ) {
1807+ return None ;
1808+ }
1809+ let mut index: u32 = 0 ;
1810+ for byte in bytes {
1811+ let digit = byte. wrapping_sub ( b'0' ) ;
1812+ if digit > 9 {
1813+ return None ;
1814+ }
1815+ // Ten digits still overflow above `4294967295`, so the accumulation is
1816+ // checked rather than merely bounded by the length.
1817+ index = index. checked_mul ( 10 ) ?. checked_add ( u32:: from ( digit) ) ?;
1818+ }
1819+ ( index < u32:: MAX ) . then_some ( index)
17941820}
17951821
17961822fn is_array_index_key ( key : & str ) -> bool {
17971823 array_index_property_key ( key) . is_some ( )
17981824}
17991825
18001826#[ cfg( test) ]
1801- mod tests {
1802- use std:: { collections:: HashMap , mem, rc:: Rc } ;
1803-
1804- use super :: { ObjectData , ObjectLiteralShape , ObjectRef , OwnDataPropertyWrite , PropertyStorage } ;
1805- use crate :: { Property , Value } ;
1806-
1807- #[ test]
1808- fn cloned_object_is_a_pointer_sized_shared_handle ( ) {
1809- let object = ObjectRef :: new ( HashMap :: new ( ) ) ;
1810- let cloned = object. clone ( ) ;
1811-
1812- assert ! ( Rc :: ptr_eq( & object. 0 , & cloned. 0 ) ) ;
1813- assert ! ( object. ptr_eq( & cloned) ) ;
1814- assert_eq ! (
1815- mem:: size_of:: <ObjectRef >( ) ,
1816- mem:: size_of:: <Rc <ObjectData >>( )
1817- ) ;
1818- }
1819-
1820- #[ test]
1821- fn ordinary_object_keeps_cold_state_unallocated ( ) {
1822- let object = ObjectRef :: new ( HashMap :: from ( [
1823- ( "a" . to_owned ( ) , Value :: Number ( 1.0 ) ) ,
1824- ( "b" . to_owned ( ) , Value :: Number ( 2.0 ) ) ,
1825- ] ) ) ;
1826-
1827- assert_eq ! ( object. get( "a" ) , Some ( Value :: Number ( 1.0 ) ) ) ;
1828- assert ! ( object. own_property_symbols( ) . is_empty( ) ) ;
1829- assert ! ( object. to_string_tag( ) . is_none( ) ) ;
1830- assert ! ( object. 0 . cold. get( ) . is_none( ) ) ;
1831- // Boxing the cold `Dynamic` property-storage payload (HashMap + Vec)
1832- // keeps this at 104 bytes instead of the 136 it cost when that
1833- // payload sized the whole `PropertyStorage` enum for every object.
1834- assert ! ( mem:: size_of:: <ObjectData >( ) <= 112 ) ;
1835- }
1836-
1837- #[ test]
1838- fn ordinary_small_object_promotes_only_after_the_compact_limit ( ) {
1839- let object = ObjectRef :: new ( HashMap :: new ( ) ) ;
1840-
1841- for index in 0 ..PropertyStorage :: SMALL_LIMIT {
1842- object. set ( format ! ( "field{index}" ) , Value :: Number ( index as f64 ) ) ;
1843- }
1844- assert ! ( matches!(
1845- & * object. 0 . properties. borrow( ) ,
1846- PropertyStorage :: Small { entries } if entries. len( ) == PropertyStorage :: SMALL_LIMIT
1847- ) ) ;
1848-
1849- let dynamic_index = PropertyStorage :: SMALL_LIMIT ;
1850- object. set (
1851- format ! ( "field{dynamic_index}" ) ,
1852- Value :: Number ( dynamic_index as f64 ) ,
1853- ) ;
1854- assert ! ( matches!(
1855- & * object. 0 . properties. borrow( ) ,
1856- PropertyStorage :: Dynamic ( dynamic)
1857- if dynamic. properties. len( ) == PropertyStorage :: SMALL_LIMIT + 1
1858- && dynamic. order. len( ) == PropertyStorage :: SMALL_LIMIT + 1
1859- ) ) ;
1860- assert_eq ! (
1861- object. own_property_names( ) ,
1862- ( 0 ..=PropertyStorage :: SMALL_LIMIT )
1863- . map( |index| format!( "field{index}" ) )
1864- . collect:: <Vec <_>>( )
1865- ) ;
1866- }
1867-
1868- #[ test]
1869- fn ordinary_object_retains_shared_static_property_key ( ) {
1870- let object = ObjectRef :: new ( HashMap :: new ( ) ) ;
1871- let key: Rc < str > = Rc :: from ( "field" ) ;
1872-
1873- object. set_shared_key ( Rc :: clone ( & key) , Value :: Number ( 1.0 ) ) ;
1874-
1875- assert ! ( matches!(
1876- & * object. 0 . properties. borrow( ) ,
1877- PropertyStorage :: Small { entries }
1878- if entries. len( ) == 1 && Rc :: ptr_eq( & entries[ 0 ] . 0 , & key)
1879- ) ) ;
1880- assert_eq ! ( object. get( "field" ) , Some ( Value :: Number ( 1.0 ) ) ) ;
1881- }
1882-
1883- #[ test]
1884- fn small_object_removal_preserves_property_order ( ) {
1885- let object = ObjectRef :: new ( HashMap :: new ( ) ) ;
1886- object. set ( "first" . to_owned ( ) , Value :: Number ( 1.0 ) ) ;
1887- object. set ( "second" . to_owned ( ) , Value :: Number ( 2.0 ) ) ;
1888- object. set ( "third" . to_owned ( ) , Value :: Number ( 3.0 ) ) ;
1889-
1890- assert ! ( object. delete_own_property( "second" ) ) ;
1891- object. set ( "second" . to_owned ( ) , Value :: Number ( 4.0 ) ) ;
1892-
1893- assert_eq ! ( object. own_property_names( ) , [ "first" , "third" , "second" ] ) ;
1894- assert ! ( matches!(
1895- & * object. 0 . properties. borrow( ) ,
1896- PropertyStorage :: Small { entries } if entries. len( ) == 3
1897- ) ) ;
1898- }
1899-
1900- #[ test]
1901- fn small_object_enumerates_indices_before_strings ( ) {
1902- let object = ObjectRef :: new ( HashMap :: new ( ) ) ;
1903- object. set ( "10" . to_owned ( ) , Value :: Number ( 10.0 ) ) ;
1904- object. set ( "label" . to_owned ( ) , Value :: Number ( 0.0 ) ) ;
1905- object. set ( "2" . to_owned ( ) , Value :: Number ( 2.0 ) ) ;
1906-
1907- assert_eq ! ( object. own_property_names( ) , [ "2" , "10" , "label" ] ) ;
1908- assert ! ( matches!(
1909- & * object. 0 . properties. borrow( ) ,
1910- PropertyStorage :: Small { entries } if entries. len( ) == 3
1911- ) ) ;
1912- }
1913-
1914- #[ test]
1915- fn existing_own_data_write_updates_or_rejects_without_slow_path ( ) {
1916- let object = ObjectRef :: new ( HashMap :: from ( [ ( "writable" . to_owned ( ) , Value :: Number ( 1.0 ) ) ] ) ) ;
1917- object. define_property (
1918- "readonly" . to_owned ( ) ,
1919- Property :: data ( Value :: Number ( 2.0 ) , true , false , true ) ,
1920- ) ;
1921-
1922- assert ! ( matches!(
1923- object. write_existing_own_data_property( "writable" , & Value :: Number ( 3.0 ) ) ,
1924- OwnDataPropertyWrite :: Written
1925- ) ) ;
1926- assert_eq ! ( object. get( "writable" ) , Some ( Value :: Number ( 3.0 ) ) ) ;
1927- assert ! ( matches!(
1928- object. write_existing_own_data_property( "readonly" , & Value :: Number ( 4.0 ) ) ,
1929- OwnDataPropertyWrite :: ReadOnly
1930- ) ) ;
1931- assert_eq ! ( object. get( "readonly" ) , Some ( Value :: Number ( 2.0 ) ) ) ;
1932- assert ! ( matches!(
1933- object. write_existing_own_data_property( "missing" , & Value :: Number ( 5.0 ) ) ,
1934- OwnDataPropertyWrite :: NeedsSlowPath
1935- ) ) ;
1936- }
1937-
1938- #[ test]
1939- fn literal_pair_keeps_inline_values_until_descriptor_mutation ( ) {
1940- let shape = ObjectLiteralShape :: new ( vec ! [ Rc :: from( "a" ) , Rc :: from( "b" ) ] ) ;
1941- let object =
1942- ObjectRef :: with_literal_pair ( shape, [ Value :: Number ( 1.0 ) , Value :: Number ( 2.0 ) ] , None ) ;
1943-
1944- assert ! ( matches!(
1945- & * object. 0 . properties. borrow( ) ,
1946- PropertyStorage :: ShapedPair { .. }
1947- ) ) ;
1948- assert ! ( matches!(
1949- object. write_existing_own_data_property( "a" , & Value :: Number ( 3.0 ) ) ,
1950- OwnDataPropertyWrite :: Written
1951- ) ) ;
1952- assert_eq ! ( object. get( "a" ) , Some ( Value :: Number ( 3.0 ) ) ) ;
1953- assert ! ( matches!(
1954- & * object. 0 . properties. borrow( ) ,
1955- PropertyStorage :: ShapedPair { .. }
1956- ) ) ;
1957-
1958- object. define_property (
1959- "a" . to_owned ( ) ,
1960- Property :: data ( Value :: Number ( 4.0 ) , false , false , true ) ,
1961- ) ;
1962- assert ! ( matches!(
1963- & * object. 0 . properties. borrow( ) ,
1964- PropertyStorage :: Dynamic ( _)
1965- ) ) ;
1966- let descriptor = object. own_property ( "a" ) . expect ( "defined property" ) ;
1967- assert_eq ! ( descriptor. value, Value :: Number ( 4.0 ) ) ;
1968- assert ! ( !descriptor. enumerable) ;
1969- assert ! ( !descriptor. writable) ;
1970- }
1971-
1972- #[ test]
1973- fn module_namespace_own_data_write_stays_on_slow_path ( ) {
1974- let object = ObjectRef :: new ( HashMap :: from ( [ ( "exported" . to_owned ( ) , Value :: Number ( 1.0 ) ) ] ) ) ;
1975- object. mark_module_namespace_exotic ( ) ;
1976-
1977- assert ! ( matches!(
1978- object. write_existing_own_data_property( "exported" , & Value :: Number ( 2.0 ) ) ,
1979- OwnDataPropertyWrite :: NeedsSlowPath
1980- ) ) ;
1981- assert_eq ! ( object. get( "exported" ) , Some ( Value :: Number ( 1.0 ) ) ) ;
1982- }
1983- }
1827+ mod tests;
0 commit comments