Skip to content

Commit e87fe3d

Browse files
Propagate nested container dirty ids
1 parent d330f2c commit e87fe3d

4 files changed

Lines changed: 68 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
- Desugar `relation`s to `constructor`s to simplify the language and implementation. Relations no longer return unit `()` values.
67
- Refactored API to use [`TermId`] more consistently instead of `Term` where possible, simplifying egglog code.
78
- **Typed primitive surface for seminaive safety (#772).** Custom primitives now pick one of `PurePrim` / `ReadPrim` / `WritePrim` / `FullPrim` based on what the body needs, and register via the matching `add_*_primitive`. Rust enforces capability bounds via the state wrapper passed to the body; the egglog typechecker enforces context bounds. See the `egglog::exec_state` module docs and the `*Prim` trait docs for the full picture. Migration: `rust_rule` callbacks now take `&mut WriteState` (replacing `RustRuleContext`); a new `rust_rule_full` gives action callbacks read access. Higher-order primitives over `unstable-fn` values dispatch via `state.apply_function(&fc, args)`.

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 seen = summary.dirty_ids.clone();
225+
let mut frontier = seen.clone();
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> {

tests/container-fail.egg

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,20 @@
2020
(let $resolved (l (vec-of (k (b)))))
2121
(run-schedule (saturate (run)))
2222
(check (= $resolved (b)))
23+
24+
;; Regression for nested container dirty propagation. The inner Vec rebuilds in
25+
;; place, making the outer Vec newly matchable without changing the outer id.
26+
(sort VVE (Vec VE))
27+
(constructor p (VVE) E)
28+
29+
(rewrite (p (vec-of (vec-of (b)))) (b))
30+
31+
(push)
32+
(let $nested (p (vec-of (vec-of (w (b))))))
33+
(run-schedule (saturate (run)))
34+
(check (= $nested (b)))
35+
(pop)
36+
37+
(let $nested-resolved (p (vec-of (vec-of (b)))))
38+
(run-schedule (saturate (run)))
39+
(check (= $nested-resolved (b)))

tests/snapshots/files__shared_snapshot_container_fail.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ expression: snapshot_content_across_treatments
55
((b 1)
66
(k 1)
77
(l 1)
8+
(p 1)
89
(w 0))

0 commit comments

Comments
 (0)