@@ -668,26 +668,40 @@ impl Table {
668668 guard : self . 0 . lua . lock ( ) ,
669669 table : self ,
670670 index : 1 ,
671+ len : None ,
671672 _phantom : PhantomData ,
672673 }
673674 }
674675
675676 /// Iterates over the sequence part of the table, invoking the given closure on each value.
677+ ///
678+ /// This methods is similar to [`Table::sequence_values`], but optimized for performance.
676679 #[ doc( hidden) ]
677- pub fn for_each_value < V > ( & self , mut f : impl FnMut ( V ) -> Result < ( ) > ) -> Result < ( ) >
678- where
679- V : FromLua ,
680- {
680+ pub fn for_each_value < V : FromLua > ( & self , f : impl FnMut ( V ) -> Result < ( ) > ) -> Result < ( ) > {
681+ self . for_each_value_by_len ( None , f)
682+ }
683+
684+ fn for_each_value_by_len < V : FromLua > (
685+ & self ,
686+ len : impl Into < Option < usize > > ,
687+ mut f : impl FnMut ( V ) -> Result < ( ) > ,
688+ ) -> Result < ( ) > {
689+ let len = len. into ( ) ;
681690 let lua = self . 0 . lua . lock ( ) ;
682691 let state = lua. state ( ) ;
683692 unsafe {
684693 let _sg = StackGuard :: new ( state) ;
685694 check_stack ( state, 4 ) ?;
686695
687696 lua. push_ref ( & self . 0 ) ;
688- let len = ffi:: lua_rawlen ( state, -1 ) ;
689- for i in 1 ..=len {
690- ffi:: lua_rawgeti ( state, -1 , i as _ ) ;
697+ for i in 1 .. {
698+ if len. map ( |len| i > len) . unwrap_or ( false ) {
699+ break ;
700+ }
701+ let t = ffi:: lua_rawgeti ( state, -1 , i as _ ) ;
702+ if len. is_none ( ) && t == ffi:: LUA_TNIL {
703+ break ;
704+ }
691705 f ( V :: from_stack ( -1 , & lua) ?) ?;
692706 ffi:: lua_pop ( state, 1 ) ;
693707 }
@@ -720,8 +734,9 @@ impl Table {
720734 Ok ( ( ) )
721735 }
722736
737+ /// Checks if the table has the array metatable attached.
723738 #[ cfg( feature = "serde" ) ]
724- pub ( crate ) fn is_array ( & self ) -> bool {
739+ fn has_array_metatable ( & self ) -> bool {
725740 let lua = self . 0 . lua . lock ( ) ;
726741 let state = lua. state ( ) ;
727742 unsafe {
@@ -737,6 +752,70 @@ impl Table {
737752 }
738753 }
739754
755+ /// If the table is an array, returns the number of non-nil elements and max index.
756+ ///
757+ /// Returns `None` if the table is not an array.
758+ ///
759+ /// This operation has O(n) complexity.
760+ #[ cfg( feature = "serde" ) ]
761+ fn find_array_len ( & self ) -> Option < ( usize , usize ) > {
762+ let lua = self . 0 . lua . lock ( ) ;
763+ let ref_thread = lua. ref_thread ( ) ;
764+ unsafe {
765+ let _sg = StackGuard :: new ( ref_thread) ;
766+
767+ let ( mut count, mut max_index) = ( 0 , 0 ) ;
768+ ffi:: lua_pushnil ( ref_thread) ;
769+ while ffi:: lua_next ( ref_thread, self . 0 . index ) != 0 {
770+ if ffi:: lua_type ( ref_thread, -2 ) != ffi:: LUA_TNUMBER {
771+ return None ;
772+ }
773+
774+ let k = ffi:: lua_tonumber ( ref_thread, -2 ) ;
775+ if k. trunc ( ) != k || k < 1.0 {
776+ return None ;
777+ }
778+ max_index = std:: cmp:: max ( max_index, k as usize ) ;
779+ count += 1 ;
780+ ffi:: lua_pop ( ref_thread, 1 ) ;
781+ }
782+ Some ( ( count, max_index) )
783+ }
784+ }
785+
786+ /// Determines if the table should be encoded as an array or a map.
787+ ///
788+ /// The algorithm is the following:
789+ /// 1. If `detect_mixed_tables` is enabled, iterate over all keys in the table checking is they
790+ /// all are positive integers. If non-array key is found, return `None` (encode as map).
791+ /// Otherwise check the sparsity of the array. Too sparse arrays are encoded as maps.
792+ ///
793+ /// 2. If `detect_mixed_tables` is disabled, check if the table has a positive length or has the
794+ /// array metatable. If so, encode as array. If the table is empty and
795+ /// `encode_empty_tables_as_array` is enabled, encode as array.
796+ ///
797+ /// Returns the length of the array if it should be encoded as an array.
798+ #[ cfg( feature = "serde" ) ]
799+ pub ( crate ) fn encode_as_array ( & self , options : crate :: serde:: de:: Options ) -> Option < usize > {
800+ if options. detect_mixed_tables {
801+ if let Some ( ( len, max_idx) ) = self . find_array_len ( ) {
802+ // If the array is too sparse, serialize it as a map instead
803+ if len < 10 || len * 2 >= max_idx {
804+ return Some ( max_idx) ;
805+ }
806+ }
807+ } else {
808+ let len = self . raw_len ( ) ;
809+ if len > 0 || self . has_array_metatable ( ) {
810+ return Some ( len) ;
811+ }
812+ if options. encode_empty_tables_as_array && self . is_empty ( ) {
813+ return Some ( 0 ) ;
814+ }
815+ }
816+ None
817+ }
818+
740819 #[ cfg( feature = "luau" ) ]
741820 #[ inline( always) ]
742821 fn check_readonly_write ( & self , lua : & RawLua ) -> Result < ( ) > {
@@ -980,6 +1059,15 @@ impl<'a> SerializableTable<'a> {
9801059 }
9811060}
9821061
1062+ impl < V > TableSequence < ' _ , V > {
1063+ /// Sets the length (hint) of the sequence.
1064+ #[ cfg( feature = "serde" ) ]
1065+ pub ( crate ) fn with_len ( mut self , len : usize ) -> Self {
1066+ self . len = Some ( len) ;
1067+ self
1068+ }
1069+ }
1070+
9831071#[ cfg( feature = "serde" ) ]
9841072impl Serialize for SerializableTable < ' _ > {
9851073 fn serialize < S > ( & self , serializer : S ) -> StdResult < S :: Ok , S :: Error >
@@ -1001,14 +1089,10 @@ impl Serialize for SerializableTable<'_> {
10011089 let _guard = RecursionGuard :: new ( self . table , visited) ;
10021090
10031091 // Array
1004- let len = self . table . raw_len ( ) ;
1005- if len > 0
1006- || self . table . is_array ( )
1007- || ( self . options . encode_empty_tables_as_array && self . table . is_empty ( ) )
1008- {
1092+ if let Some ( len) = self . table . encode_as_array ( self . options ) {
10091093 let mut seq = serializer. serialize_seq ( Some ( len) ) ?;
10101094 let mut serialize_err = None ;
1011- let res = self . table . for_each_value :: < Value > ( |value| {
1095+ let res = self . table . for_each_value_by_len :: < Value > ( len , |value| {
10121096 let skip = check_value_for_skip ( & value, self . options , visited)
10131097 . map_err ( |err| Error :: SerializeError ( err. to_string ( ) ) ) ?;
10141098 if skip {
@@ -1132,13 +1216,11 @@ pub struct TableSequence<'a, V> {
11321216 guard : LuaGuard ,
11331217 table : & ' a Table ,
11341218 index : Integer ,
1219+ len : Option < usize > ,
11351220 _phantom : PhantomData < V > ,
11361221}
11371222
1138- impl < V > Iterator for TableSequence < ' _ , V >
1139- where
1140- V : FromLua ,
1141- {
1223+ impl < V : FromLua > Iterator for TableSequence < ' _ , V > {
11421224 type Item = Result < V > ;
11431225
11441226 fn next ( & mut self ) -> Option < Self :: Item > {
@@ -1152,7 +1234,7 @@ where
11521234
11531235 lua. push_ref ( & self . table . 0 ) ;
11541236 match ffi:: lua_rawgeti ( state, -1 , self . index ) {
1155- ffi:: LUA_TNIL => None ,
1237+ ffi:: LUA_TNIL if self . index as usize > self . len . unwrap_or ( 0 ) => None ,
11561238 _ => {
11571239 self . index += 1 ;
11581240 Some ( V :: from_stack ( -1 , lua) )
0 commit comments