@@ -81,6 +81,8 @@ pub struct ContainerValues {
8181/// `dirty_ids` is narrower: it records container ids whose semantics changed
8282/// while their stored outer id stayed stable. Ordinary table rebuild already
8383/// handles changed-id cases; these ids need a follow-up parent-row refresh.
84+ /// This includes containers that changed directly and containers whose
85+ /// contained containers changed in place.
8486///
8587/// For example, `l(vec-of(w(k(b))))` can rebuild to `l(vec-of(k(b)))` without
8688/// changing the `Vec` id. The row is now newly matchable, but seminaive will
@@ -174,7 +176,7 @@ impl ContainerValues {
174176 // We may attempt an incremental rebuild.
175177 self . subset_tracker . recent_updates ( table_id, table)
176178 } ) ;
177- if parallelize_inter_container_op ( self . data . next_id ( ) . index ( ) ) {
179+ let mut summary = if parallelize_inter_container_op ( self . data . next_id ( ) . index ( ) ) {
178180 self . data
179181 . iter_mut ( )
180182 . zip ( std:: iter:: repeat_with ( || exec_state. clone ( ) ) )
@@ -202,6 +204,38 @@ impl ContainerValues {
202204 ) ) ;
203205 }
204206 summary
207+ } ;
208+ self . expand_dirty_id_closure ( & mut summary) ;
209+ summary
210+ }
211+
212+ /// Add ancestor containers to the dirty-id set until it is transitively closed.
213+ ///
214+ /// A rebuild can change a container's semantics in place without changing
215+ /// its id. If that container is itself stored inside another container,
216+ /// the parent container has also changed semantically even though no direct
217+ /// rebuild touched its contents. For example, with
218+ /// `(p (vec-of (vec-of (w (b)))))` and `(rewrite (w x) x)`, the inner
219+ /// `Vec` rebuilds in place to `vec-of (b)`. Without this closure, only the
220+ /// inner `Vec` id is dirty; the outer `Vec` row is not retimestamped, so a
221+ /// later rule like `(rewrite (p (vec-of (vec-of (b)))) (b))` can miss the
222+ /// newly matchable parent row.
223+ fn expand_dirty_id_closure ( & self , summary : & mut ContainerRebuildSummary ) {
224+ let mut frontier = summary. dirty_ids . clone ( ) ;
225+ let mut seen = frontier. iter ( ) . copied ( ) . collect :: < IndexSet < _ > > ( ) ;
226+
227+ while !frontier. is_empty ( ) {
228+ let mut next = IndexSet :: default ( ) ;
229+ for ( _, env) in self . data . iter ( ) {
230+ env. extend_containers_containing ( & frontier, & mut next) ;
231+ }
232+ frontier. clear ( ) ;
233+ for value in next {
234+ if seen. insert ( value) {
235+ summary. note_dirty_id ( value) ;
236+ frontier. insert ( value) ;
237+ }
238+ }
205239 }
206240 }
207241
@@ -252,6 +286,12 @@ pub trait DynamicContainerEnv: Any + dyn_clone::DynClone + Send + Sync {
252286 subset : Option < SubsetRef > ,
253287 exec_state : & mut ExecutionState ,
254288 ) -> ContainerRebuildSummary ;
289+ /// Add ids for containers in this environment that contain any `values`.
290+ ///
291+ /// This uses the container content index populated from
292+ /// [`ContainerValue::iter`] and lets callers climb from dirty child ids to
293+ /// all directly containing parent container ids.
294+ fn extend_containers_containing ( & self , values : & IndexSet < Value > , out : & mut IndexSet < Value > ) ;
255295}
256296
257297// Implements `Clone` for `Box<dyn DynamicContainerEnv>`.
@@ -302,6 +342,14 @@ impl<C: ContainerValue> DynamicContainerEnv for ContainerEnv<C> {
302342 }
303343 self . apply_rebuild_nonincremental ( rebuilder, exec_state)
304344 }
345+
346+ fn extend_containers_containing ( & self , values : & IndexSet < Value > , out : & mut IndexSet < Value > ) {
347+ for value in values {
348+ if let Some ( containers) = self . val_index . get ( value) {
349+ out. extend ( containers. iter ( ) . copied ( ) ) ;
350+ }
351+ }
352+ }
305353}
306354
307355impl < C : ContainerValue > ContainerEnv < C > {
0 commit comments