Skip to content

Commit b3d022b

Browse files
authored
Add incremental invalidation engine (#641)
Replaces the old `remove_definitions_for_document` + `invalidate_ancestor_chains` approach with a targeted invalidation engine. When a file is updated or deleted, the engine traces through the `name_dependents` reverse index to invalidate only the affected declarations, names, and references — instead of requiring a full graph rebuild. ## How it works `consume_document_changes()` (renamed from `update()`) and `delete_document()` run a three-step pipeline: 1. **`invalidate`** — read-only scan that seeds a worklist from old/new definitions and references, building a `pending_detachments` side table for definitions that need to be detached from their declarations 2. **`remove_document_data`** — cleans up phase 1 data (definitions, references, names, strings) from maps 3. **`extend`** — merges new content and queues work items for the resolver The resolver still does `clear_declarations` + full rebuild. Wiring it to drain `pending_work` incrementally is a follow-up. ### Invalidation worklist The worklist processes three item types: - **`Declaration`** — two modes: - *Remove*: no definitions remain or owner was already removed (orphaned). Cascades to members, singleton class, and descendants. Orphaned definitions are re-queued for re-resolution (e.g. `class Foo::Bar` survives even if `Foo` changes from a module to an alias). - *Update*: declaration survives but its ancestor chain may have changed (mixin added/removed, superclass changed). Clears ancestors/descendants and re-queues ancestor resolution. - **`Name`** — structural dependency broken (name's nesting or parent scope removed). Unresolves the name and cascades to all dependents. - **`References`** — ancestor context changed, but the name itself is still valid. Needed for mixin-related invalidation: ```ruby class Foo < Bar CONST end # Another file adds: class Foo include Baz # Foo's ancestors changed, so references like CONST need re-evaluation end ``` ### Cascade differentiation The `name_dependents` reverse index distinguishes `ChildName` (compact syntax `Foo::Bar`) from `NestedName` (nested syntax `module Foo; class Bar; end; end`): - **Structural cascade** (name removed): both `ChildName` and `NestedName` → `Name` - **Ancestor-triggered cascade** (mixin changed): `ChildName` → `Name` (resolves through parent), `NestedName` → `References` (only references need rechecking)
1 parent 00c630c commit b3d022b

6 files changed

Lines changed: 1603 additions & 135 deletions

File tree

rust/rubydex-sys/src/graph_api.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ mod tests {
643643
indexer.index();
644644

645645
let mut graph = Graph::new();
646-
graph.update(indexer.local_graph());
646+
graph.consume_document_changes(indexer.local_graph());
647647
let mut resolver = Resolver::new(&mut graph);
648648
resolver.resolve_all();
649649

rust/rubydex/src/indexing.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ impl Job for IndexingJob {
9595
/// Indexes a single source string in memory, dispatching to the appropriate indexer based on `language_id`.
9696
pub fn index_source(graph: &mut Graph, uri: &str, source: &str, language_id: &LanguageId) {
9797
let local_graph = build_local_graph(uri.to_string(), source, language_id);
98-
graph.update(local_graph);
98+
graph.consume_document_changes(local_graph);
9999
}
100100

101101
/// Indexes the given paths, reading the content from disk and populating the given `Graph` instance.
@@ -123,7 +123,7 @@ pub fn index_files(graph: &mut Graph, paths: Vec<PathBuf>) -> Vec<Errors> {
123123

124124
// Merge graphs as they arrive, overlapping with indexing work on other threads.
125125
while let Ok(local_graph) = local_graphs_rx.recv() {
126-
graph.update(local_graph);
126+
graph.consume_document_changes(local_graph);
127127
}
128128

129129
for handle in handles {

rust/rubydex/src/model/declaration.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,10 @@ impl Namespace {
482482
all_namespaces!(self, it => it.member(str_id))
483483
}
484484

485+
pub fn remove_member(&mut self, str_id: &StringId) -> Option<DeclarationId> {
486+
all_namespaces!(self, it => it.remove_member(str_id))
487+
}
488+
485489
#[must_use]
486490
pub fn singleton_class(&self) -> Option<&DeclarationId> {
487491
all_namespaces!(self, it => it.singleton_class_id())

0 commit comments

Comments
 (0)