@@ -37,11 +37,13 @@ struct Decoder<'a> {
3737 metastack : Vec < Vec < PickleValue > > ,
3838 /// Tracks which memo indices are bound to each stack slot (parallel to stack).
3939 /// When BINPUT stores from stack top, the memo index is recorded here.
40- /// After in-place mutations (SETITEMS, APPENDS, etc.), stale memo entries
41- /// are updated via sync_memo_top().
4240 stack_memo : Vec < Vec < usize > > ,
4341 /// Saved stack_memo during MARK (parallel to metastack).
4442 meta_stack_memo : Vec < Vec < Vec < usize > > > ,
43+ /// Dirty flags parallel to memo: true means the memo entry is stale
44+ /// (the owning stack slot was mutated after BINPUT stored the value).
45+ /// Resolved lazily at BINGET or eagerly when the slot is popped.
46+ dirty_memo : Vec < bool > ,
4547}
4648
4749impl < ' a > Decoder < ' a > {
@@ -54,6 +56,7 @@ impl<'a> Decoder<'a> {
5456 metastack : Vec :: with_capacity ( 4 ) ,
5557 stack_memo : Vec :: with_capacity ( 16 ) ,
5658 meta_stack_memo : Vec :: with_capacity ( 4 ) ,
59+ dirty_memo : Vec :: with_capacity ( 16 ) ,
5760 }
5861 }
5962
@@ -310,7 +313,7 @@ impl<'a> Decoder<'a> {
310313 ) ) ;
311314 }
312315 }
313- self . sync_memo_top ( ) ? ;
316+ self . mark_top_dirty ( ) ;
314317 }
315318 APPENDS => {
316319 let items = self . pop_mark ( ) ?;
@@ -335,7 +338,7 @@ impl<'a> Decoder<'a> {
335338 ) ) ;
336339 }
337340 }
338- self . sync_memo_top ( ) ? ;
341+ self . mark_top_dirty ( ) ;
339342 }
340343
341344 // -- Dict --
@@ -369,7 +372,7 @@ impl<'a> Decoder<'a> {
369372 ) ) ;
370373 }
371374 }
372- self . sync_memo_top ( ) ? ;
375+ self . mark_top_dirty ( ) ;
373376 }
374377 SETITEMS => {
375378 let items = self . pop_mark ( ) ?;
@@ -395,7 +398,7 @@ impl<'a> Decoder<'a> {
395398 ) ) ;
396399 }
397400 }
398- self . sync_memo_top ( ) ? ;
401+ self . mark_top_dirty ( ) ;
399402 }
400403
401404 // -- Set/FrozenSet (protocol 4) --
@@ -410,7 +413,7 @@ impl<'a> Decoder<'a> {
410413 "ADDITEMS on non-set" . to_string ( ) ,
411414 ) ) ;
412415 }
413- self . sync_memo_top ( ) ? ;
416+ self . mark_top_dirty ( ) ;
414417 }
415418 FROZENSET => {
416419 let items = self . pop_mark ( ) ?;
@@ -609,7 +612,7 @@ impl<'a> Decoder<'a> {
609612 if let Some ( top_bindings) = self . stack_memo . last_mut ( ) {
610613 top_bindings. extend ( obj_bindings) ;
611614 }
612- self . sync_memo_top ( ) ? ;
615+ self . mark_top_dirty ( ) ;
613616 }
614617 NEWOBJ => {
615618 let args = self . pop_value ( ) ?;
@@ -783,8 +786,29 @@ impl<'a> Decoder<'a> {
783786
784787 #[ inline]
785788 fn pop_value ( & mut self ) -> Result < PickleValue , CodecError > {
786- self . stack_memo . pop ( ) ;
787- self . stack . pop ( ) . ok_or ( CodecError :: StackUnderflow )
789+ let bindings = self . stack_memo . pop ( ) . unwrap_or_default ( ) ;
790+ let val = self . stack . pop ( ) . ok_or ( CodecError :: StackUnderflow ) ?;
791+ // Sync any dirty memo entries before the value leaves the stack
792+ if !bindings. is_empty ( ) {
793+ let mut need_clone = false ;
794+ for & idx in & bindings {
795+ if idx < self . dirty_memo . len ( ) && self . dirty_memo [ idx] {
796+ need_clone = true ;
797+ break ;
798+ }
799+ }
800+ if need_clone {
801+ for & idx in & bindings {
802+ if idx < self . dirty_memo . len ( ) && self . dirty_memo [ idx] {
803+ if idx < self . memo . len ( ) {
804+ self . memo [ idx] = val. clone ( ) ;
805+ }
806+ self . dirty_memo [ idx] = false ;
807+ }
808+ }
809+ }
810+ }
811+ Ok ( val)
788812 }
789813
790814 #[ inline]
@@ -802,9 +826,19 @@ impl<'a> Decoder<'a> {
802826 // Take the current stack (everything since MARK) as the result.
803827 // This is a pointer swap — no element-by-element drain needed.
804828 let items = std:: mem:: take ( & mut self . stack ) ;
829+ let slot_memos = std:: mem:: take ( & mut self . stack_memo ) ;
805830
806- // Also discard the memo bindings for popped items
807- self . stack_memo . clear ( ) ;
831+ // Sync dirty memo entries for all popped slots before values are consumed
832+ for ( val, bindings) in items. iter ( ) . zip ( slot_memos. iter ( ) ) {
833+ for & idx in bindings {
834+ if idx < self . dirty_memo . len ( ) && self . dirty_memo [ idx] {
835+ if idx < self . memo . len ( ) {
836+ self . memo [ idx] = val. clone ( ) ;
837+ }
838+ self . dirty_memo [ idx] = false ;
839+ }
840+ }
841+ }
808842
809843 // Restore the previous stack from metastack
810844 if let Some ( old_stack) = self . metastack . pop ( ) {
@@ -825,18 +859,48 @@ impl<'a> Decoder<'a> {
825859 }
826860 if idx >= self . memo . len ( ) {
827861 self . memo . resize ( idx + 1 , PickleValue :: None ) ;
862+ self . dirty_memo . resize ( idx + 1 , false ) ;
828863 }
829864 self . memo [ idx] = val;
865+ self . dirty_memo [ idx] = false ;
830866 Ok ( ( ) )
831867 }
832868
833- fn memo_get ( & self , idx : usize ) -> Result < PickleValue , CodecError > {
869+ /// Get a memo entry, lazily resolving dirty (stale) entries first.
870+ fn memo_get ( & mut self , idx : usize ) -> Result < PickleValue , CodecError > {
871+ if idx < self . dirty_memo . len ( ) && self . dirty_memo [ idx] {
872+ self . resolve_dirty_memo ( idx) ;
873+ }
834874 self . memo
835875 . get ( idx)
836876 . cloned ( )
837877 . ok_or_else ( || CodecError :: InvalidData ( format ! ( "memo index {idx} not found" ) ) )
838878 }
839879
880+ /// Resolve a dirty memo entry by finding its live value on the stack.
881+ fn resolve_dirty_memo ( & mut self , memo_idx : usize ) {
882+ // Search current stack for the slot that owns this memo binding
883+ for ( si, bindings) in self . stack_memo . iter ( ) . enumerate ( ) {
884+ if bindings. contains ( & memo_idx) {
885+ self . memo [ memo_idx] = self . stack [ si] . clone ( ) ;
886+ self . dirty_memo [ memo_idx] = false ;
887+ return ;
888+ }
889+ }
890+ // Search metastack (values saved by MARK)
891+ for ( mi, meta_sm) in self . meta_stack_memo . iter ( ) . enumerate ( ) {
892+ for ( si, bindings) in meta_sm. iter ( ) . enumerate ( ) {
893+ if bindings. contains ( & memo_idx) {
894+ self . memo [ memo_idx] = self . metastack [ mi] [ si] . clone ( ) ;
895+ self . dirty_memo [ memo_idx] = false ;
896+ return ;
897+ }
898+ }
899+ }
900+ // Value was already consumed from stack — memo has the last stored value
901+ self . dirty_memo [ memo_idx] = false ;
902+ }
903+
840904 /// Record that the current stack top was stored in memo at `idx`.
841905 #[ inline]
842906 fn record_memo_binding ( & mut self , idx : usize ) {
@@ -845,21 +909,18 @@ impl<'a> Decoder<'a> {
845909 }
846910 }
847911
848- /// After an in-place mutation of the stack top (SETITEMS, APPENDS, etc.),
849- /// update any memo entries that were cloned from the pre-mutation state.
850- fn sync_memo_top ( & mut self ) -> Result < ( ) , CodecError > {
912+ /// Mark memo entries bound to the current stack top as dirty (stale).
913+ /// Called after in-place mutations (SETITEMS, APPENDS, etc.) instead of
914+ /// eagerly cloning. Resolution is deferred to memo_get() or pop_value().
915+ #[ inline]
916+ fn mark_top_dirty ( & mut self ) {
851917 if let Some ( bindings) = self . stack_memo . last ( ) {
852- if !bindings. is_empty ( ) {
853- let new_val = self . stack . last ( )
854- . ok_or ( CodecError :: StackUnderflow ) ?. clone ( ) ;
855- for & idx in bindings {
856- if idx < self . memo . len ( ) {
857- self . memo [ idx] = new_val. clone ( ) ;
858- }
918+ for & idx in bindings {
919+ if idx < self . dirty_memo . len ( ) {
920+ self . dirty_memo [ idx] = true ;
859921 }
860922 }
861923 }
862- Ok ( ( ) )
863924 }
864925}
865926
0 commit comments