@@ -115,6 +115,47 @@ fn tape_top_level_scalars() {
115115 assert_eq ! ( build_tape( b"null" ) . unwrap( ) . entries. len( ) , 1 ) ;
116116}
117117
118+ #[ test]
119+ fn recursive_materializer_reserves_exact_spill_per_object_depth ( ) {
120+ let input = br#"{"a":1,"nested":{"n0":0,"n1":1,"n2":2,"n3":3,"n4":4},"b":2}"# ;
121+ let tape = build_tape ( input) . expect ( "valid tape" ) ;
122+ let nested_key = crate :: string:: js_string_from_bytes ( b"nested" . as_ptr ( ) , 6 ) ;
123+
124+ crate :: gc:: gc_suppress ( ) ;
125+ let value = unsafe { materialize ( & tape, input) } ;
126+ let object = ( value. bits ( ) & crate :: value:: POINTER_MASK ) as * const crate :: ObjectHeader ;
127+ let nested = crate :: object:: js_object_get_field_by_name ( object, nested_key) ;
128+ let nested = ( nested. bits ( ) & crate :: value:: POINTER_MASK ) as * const crate :: ObjectHeader ;
129+
130+ unsafe {
131+ assert_eq ! (
132+ ( * object) . field_count,
133+ crate :: object:: INLINE_SLOT_FLOOR as u32 ,
134+ "known width must not enlarge the primary object"
135+ ) ;
136+ let spill =
137+ crate :: object:: test_spill_buffer_addr ( object as usize ) as * const crate :: ArrayHeader ;
138+ assert ! ( !spill. is_null( ) ) ;
139+ assert_eq ! ( ( * spill) . capacity, 3 , "count only outer-object keys" ) ;
140+ assert_eq ! ( ( * spill) . length, 3 ) ;
141+
142+ assert_eq ! (
143+ ( * nested) . field_count,
144+ crate :: object:: INLINE_SLOT_FLOOR as u32
145+ ) ;
146+ let nested_spill =
147+ crate :: object:: test_spill_buffer_addr ( nested as usize ) as * const crate :: ArrayHeader ;
148+ assert ! ( !nested_spill. is_null( ) ) ;
149+ assert_eq ! (
150+ ( * nested_spill) . capacity,
151+ 5 ,
152+ "reserve the nested width exactly"
153+ ) ;
154+ assert_eq ! ( ( * nested_spill) . length, 5 ) ;
155+ }
156+ crate :: gc:: gc_unsuppress ( ) ;
157+ }
158+
118159#[ test]
119160fn iterative_materializer_preserves_nested_objects_arrays_and_duplicate_keys ( ) {
120161 let input = br#"{"a":[1,true,"x"],"a":{"b":2}}"# ;
@@ -136,6 +177,28 @@ fn iterative_materializer_preserves_nested_objects_arrays_and_duplicate_keys() {
136177 crate :: json:: parse_root_restore ( saved_roots) ;
137178}
138179
180+ #[ test]
181+ fn iterative_materializer_reserves_exact_spill_without_widening_object ( ) {
182+ let input = br#"{"f0":0,"f1":1,"f2":2,"f3":3,"f4":4}"# ;
183+ let tape = build_tape ( input) . expect ( "valid tape" ) ;
184+
185+ crate :: gc:: gc_suppress ( ) ;
186+ let value = unsafe { materialize_iterative ( & tape. entries , input) } . expect ( "materializes" ) ;
187+ let object = ( value. bits ( ) & crate :: value:: POINTER_MASK ) as * const crate :: ObjectHeader ;
188+ unsafe {
189+ assert_eq ! (
190+ ( * object) . field_count,
191+ crate :: object:: INLINE_SLOT_FLOOR as u32
192+ ) ;
193+ let spill =
194+ crate :: object:: test_spill_buffer_addr ( object as usize ) as * const crate :: ArrayHeader ;
195+ assert ! ( !spill. is_null( ) ) ;
196+ assert_eq ! ( ( * spill) . capacity, 5 ) ;
197+ assert_eq ! ( ( * spill) . length, 5 ) ;
198+ }
199+ crate :: gc:: gc_unsuppress ( ) ;
200+ }
201+
139202/// `TapeEntry` is 12 bytes (u32 + u8 + padding + u32). Keeping
140203/// this compact matters for tape-size parity with parse output:
141204/// a 1 MB JSON blob with ~20k tokens should build a ~240 KB tape,
0 commit comments