Skip to content

Commit e564e46

Browse files
Merge pull request #888 from saulshanabrook/codex/split-nested-container-dirty-closure
Propagate dirty ids through nested containers
2 parents 36de434 + 194d3ef commit e564e46

5 files changed

Lines changed: 77 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [Unreleased] - ReleaseDate
44

5+
- Fix seminaive matching after nested containers rebuild in place by propagating dirty container ids through parent containers.
56
- Render nullary AST calls without a trailing space, e.g. (foo) instead of (foo ).
67
- Add a BigRat to-i64 primitive for integral rationals.
78
- Add f64 exp, log, and sqrt primitives.

core-relations/src/containers/mod.rs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

307355
impl<C: ContainerValue> ContainerEnv<C> {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
;; Regression for PR #888, which extends the #831/#832 container refresh fix to
2+
;; nested containers. Rebuilding `w(b)` to `b` dirties the inner Vec, but the
3+
;; outer Vec still contains the same inner Vec id. Without transitive dirty-id
4+
;; closure, the `p(outer)` row is not revisited, so the rewrite below misses the
5+
;; newly matchable `p(vec-of(vec-of(b)))`.
6+
(sort E)
7+
(sort VE (Vec E))
8+
(sort VVE (Vec VE))
9+
10+
(constructor b () E)
11+
(constructor w (E) E)
12+
(constructor p (VVE) E)
13+
14+
(rewrite (w x) x)
15+
(rewrite (p (vec-of (vec-of (b)))) (b))
16+
17+
(let $nested (p (vec-of (vec-of (w (b))))))
18+
(run-schedule (saturate (run)))
19+
(check (= $nested (b)))

tests/snapshots/files__proof_unsupported_files.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ map.egg
3737
math.egg
3838
merge_read.egg
3939
multiset.egg
40+
nested-container-dirty-propagation.egg
4041
pair.egg
4142
prims.egg
4243
print-function.egg
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
source: tests/files.rs
3+
expression: snapshot_content_across_treatments
4+
---
5+
((b 1)
6+
(p 1)
7+
(w 1))

0 commit comments

Comments
 (0)