Skip to content

Commit 779653a

Browse files
committed
Store files in HashSet
1 parent b9a8807 commit 779653a

7 files changed

Lines changed: 88 additions & 48 deletions

File tree

crates/oak_db/src/inputs.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::HashSet;
2+
13
use aether_url::UrlId;
24
use oak_package_metadata::namespace::Namespace;
35

@@ -124,13 +126,16 @@ pub struct OrphanRoot {
124126
/// **Placement invariant.** Files here must have `package(db) ==
125127
/// None`. Call this setter only through `oak_scan`'s helpers,
126128
/// which keep the back-pointer and the container in sync.
129+
///
130+
/// Unordered: these are unanchored files looked up by URL, with no
131+
/// collation chain among them, so membership is all that matters.
127132
#[returns(ref)]
128-
pub files: Vec<File>,
133+
pub files: HashSet<File>,
129134
}
130135

131136
impl OrphanRoot {
132137
pub fn empty(db: &dyn Db) -> Self {
133-
Self::new(db, vec![])
138+
Self::new(db, HashSet::new())
134139
}
135140
}
136141

@@ -161,15 +166,17 @@ impl OrphanRoot {
161166
/// of them gets pulled back into a live container.
162167
#[salsa::input]
163168
pub struct StaleRoot {
169+
/// Unordered: entity-reuse storage looked up by URL, no collation chain,
170+
/// so membership is all that matters.
164171
#[returns(ref)]
165-
pub files: Vec<File>,
172+
pub files: HashSet<File>,
166173
#[returns(ref)]
167174
pub packages: Vec<Package>,
168175
}
169176

170177
impl StaleRoot {
171178
pub fn empty(db: &dyn Db) -> Self {
172-
Self::new(db, vec![], vec![])
179+
Self::new(db, HashSet::new(), vec![])
173180
}
174181
}
175182

crates/oak_db/src/tests/db.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::HashSet;
2+
13
use oak_package_metadata::namespace::Namespace;
24
use salsa::Setter;
35

@@ -73,7 +75,9 @@ fn test_file_by_url_finds_library_package_file() {
7375
fn test_file_by_url_finds_orphan_file() {
7476
let mut db = TestDb::new();
7577
let file = File::new(&db, file_url("untitled.R"), String::new(), None);
76-
db.orphan_root().set_files(&mut db).to(vec![file]);
78+
db.orphan_root()
79+
.set_files(&mut db)
80+
.to(HashSet::from([file]));
7781

7882
assert_eq!(db.file_by_url(&file_url("untitled.R")), Some(file));
7983
}

crates/oak_db/src/tests/resolver.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::collections::HashSet;
2+
13
use oak_semantic::semantic_index::DefinitionKind;
24
use oak_semantic::semantic_index::ScopeId;
35
use oak_semantic::semantic_index::SemanticCallKind;
@@ -350,7 +352,9 @@ fn test_source_anchors_to_parent_dir_when_no_workspace() {
350352
None,
351353
);
352354
let b = File::new(&db, file_url("dir/b.R"), "x <- 1\n".to_string(), None);
353-
db.orphan_root().set_files(&mut db).to(vec![a, b]);
355+
db.orphan_root()
356+
.set_files(&mut db)
357+
.to(HashSet::from([a, b]));
354358

355359
let index = a.semantic_index(&db);
356360
assert!(index.exports().contains_key("x"));
@@ -368,7 +372,9 @@ fn test_source_path_with_parent_dir_segments() {
368372
None,
369373
);
370374
let b = File::new(&db, file_url("dir/b.R"), "x <- 1\n".to_string(), None);
371-
db.orphan_root().set_files(&mut db).to(vec![a, b]);
375+
db.orphan_root()
376+
.set_files(&mut db)
377+
.to(HashSet::from([a, b]));
372378

373379
let index = a.semantic_index(&db);
374380
assert!(index.exports().contains_key("x"));

crates/oak_scan/src/inputs.rs

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
//! input structs.
1818
1919
use std::collections::HashSet;
20+
use std::hash::Hash;
2021
use std::path::PathBuf;
2122

2223
use aether_url::UrlId;
@@ -178,7 +179,7 @@ impl<DB: Db + DbInputs> DbScan for DB {
178179
};
179180

180181
let orphan = self.orphan_root();
181-
let Some(orphan_files) = with_removed(orphan.files(self), file) else {
182+
let Some(orphan_files) = with_cow_remove(orphan.files(self), file) else {
182183
// A workspace or library root holds it, nothing to do.
183184
return;
184185
};
@@ -187,7 +188,7 @@ impl<DB: Db + DbInputs> DbScan for DB {
187188
orphan.set_files(self).to(orphan_files);
188189

189190
let stale = self.stale_root();
190-
if let Some(stale_files) = with_appended(stale.files(self), file) {
191+
if let Some(stale_files) = with_cow_insert(stale.files(self), file) {
191192
stale.set_files(self).to(stale_files);
192193
}
193194
}
@@ -384,36 +385,37 @@ pub(crate) fn upsert_root_file<DB: Db + DbInputs>(
384385
/// entry. Also used by [`watch::remove_watched_file`] when a file
385386
/// disappears from disk.
386387
pub(crate) fn remove_from_pkg_files<DB: Db + DbInputs>(db: &mut DB, pkg: Package, file: File) {
387-
if let Some(files) = with_removed(pkg.files(db), file) {
388+
if let Some(files) = with_cow_filter(pkg.files(db), file) {
388389
pkg.set_files(db).to(files);
389390
return;
390391
}
391-
if let Some(scripts) = with_removed(pkg.scripts(db), file) {
392+
if let Some(scripts) = with_cow_filter(pkg.scripts(db), file) {
392393
pkg.set_scripts(db).to(scripts);
393394
}
394395
}
395396

396397
pub(crate) fn remove_from_orphan<DB: Db + DbInputs>(db: &mut DB, file: File) {
397398
let orphan = db.orphan_root();
398-
if let Some(files) = with_removed(orphan.files(db), file) {
399+
if let Some(files) = with_cow_remove(orphan.files(db), file) {
399400
orphan.set_files(db).to(files);
400401
}
401402
}
402403

403404
fn add_to_orphan_files<DB: Db + DbInputs>(db: &mut DB, file: File) {
404405
let orphan = db.orphan_root();
405-
if let Some(files) = with_appended(orphan.files(db), file) {
406+
if let Some(files) = with_cow_insert(orphan.files(db), file) {
406407
orphan.set_files(db).to(files);
407408
}
408409
}
409410

410-
/// The container with `file` appended, or `None` if it's already there.
411+
/// The ordered container with `file` appended, or `None` if it's already there.
411412
///
412413
/// `None` means nothing would change, so the caller skips the salsa write and
413-
/// the clone. This keeps the "clone only when the field actually changes"
414-
/// rule in one place, shared by every container update on `Root` / `Package` /
415-
/// `OrphanRoot`.
416-
pub(crate) fn with_appended<T: Clone + PartialEq>(files: &[T], file: T) -> Option<Vec<T>> {
414+
/// the clone. This keeps the "clone only when the field actually changes" rule
415+
/// in one place, shared by the ordered container updates on `Root` and
416+
/// `Package`. See [`with_inserted`] / [`with_discarded`] for the unordered
417+
/// `OrphanRoot` / `StaleRoot` sets.
418+
pub(crate) fn with_cow_push<T: Clone + PartialEq>(files: &[T], file: T) -> Option<Vec<T>> {
417419
if files.contains(&file) {
418420
return None;
419421
}
@@ -422,11 +424,40 @@ pub(crate) fn with_appended<T: Clone + PartialEq>(files: &[T], file: T) -> Optio
422424
Some(updated)
423425
}
424426

425-
/// The container with `file` removed, or `None` if it wasn't there. `None`
426-
/// means nothing would change, see [`with_file_appended`].
427-
pub(crate) fn with_removed<T: Clone + PartialEq>(files: &[T], file: T) -> Option<Vec<T>> {
427+
/// The ordered container with `file` removed, or `None` if it wasn't there.
428+
/// `None` means nothing would change, see [`with_appended`].
429+
pub(crate) fn with_cow_filter<T: Clone + PartialEq>(files: &[T], file: T) -> Option<Vec<T>> {
428430
if !files.contains(&file) {
429431
return None;
430432
}
431433
Some(files.iter().filter(|f| **f != file).cloned().collect())
432434
}
435+
436+
/// The set with `item` inserted, or `None` if it's already present. The
437+
/// unordered counterpart of [`with_appended`], used for the `OrphanRoot` /
438+
/// `StaleRoot` sets where membership is all that matters.
439+
pub(crate) fn with_cow_insert<T: Clone + Eq + Hash>(
440+
set: &HashSet<T>,
441+
item: T,
442+
) -> Option<HashSet<T>> {
443+
if set.contains(&item) {
444+
return None;
445+
}
446+
let mut updated = set.clone();
447+
updated.insert(item);
448+
Some(updated)
449+
}
450+
451+
/// The set with `item` removed, or `None` if it wasn't present. The unordered
452+
/// counterpart of [`with_removed`].
453+
pub(crate) fn with_cow_remove<T: Clone + Eq + Hash>(
454+
set: &HashSet<T>,
455+
item: T,
456+
) -> Option<HashSet<T>> {
457+
if !set.contains(&item) {
458+
return None;
459+
}
460+
let mut updated = set.clone();
461+
updated.remove(&item);
462+
Some(updated)
463+
}

crates/oak_scan/src/stale.rs

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ use oak_db::Root;
3535
use rustc_hash::FxHashMap;
3636
use salsa::Setter;
3737

38+
use crate::inputs::with_cow_filter;
39+
use crate::inputs::with_cow_remove;
40+
3841
/// Drop `root` from its live container, rehoming files and packages to
3942
/// `OrphanRoot` / `StaleRoot` as described in the module doc.
4043
///
@@ -77,22 +80,14 @@ pub(crate) fn set_root_stale<DB: Db + DbInputs>(
7780
if !to_orphan.is_empty() {
7881
let orphan = db.orphan_root();
7982
let mut files = orphan.files(db).clone();
80-
for file in to_orphan {
81-
if !files.contains(&file) {
82-
files.push(file);
83-
}
84-
}
83+
files.extend(to_orphan);
8584
orphan.set_files(db).to(files);
8685
}
8786

8887
let stale = db.stale_root();
8988
if !to_stale.is_empty() {
9089
let mut files = stale.files(db).clone();
91-
for file in to_stale {
92-
if !files.contains(&file) {
93-
files.push(file);
94-
}
95-
}
90+
files.extend(to_stale);
9691
stale.set_files(db).to(files);
9792
}
9893

@@ -120,22 +115,16 @@ pub(crate) fn set_root_stale<DB: Db + DbInputs>(
120115

121116
pub(crate) fn remove_from_stale_files<DB: Db + DbInputs>(db: &mut DB, file: File) {
122117
let stale = db.stale_root();
123-
if !stale.files(db).contains(&file) {
124-
return;
118+
if let Some(files) = with_cow_remove(stale.files(db), file) {
119+
stale.set_files(db).to(files);
125120
}
126-
let mut files = stale.files(db).clone();
127-
files.retain(|f| *f != file);
128-
stale.set_files(db).to(files);
129121
}
130122

131123
pub(crate) fn remove_from_stale_packages<DB: Db + DbInputs>(db: &mut DB, pkg: Package) {
132124
let stale = db.stale_root();
133-
if !stale.packages(db).contains(&pkg) {
134-
return;
125+
if let Some(packages) = with_cow_filter(stale.packages(db), pkg) {
126+
stale.set_packages(db).to(packages);
135127
}
136-
let mut packages = stale.packages(db).clone();
137-
packages.retain(|p| *p != pkg);
138-
stale.set_packages(db).to(packages);
139128
}
140129

141130
/// Look up a stale `File` by URL. The scanner's upsert helpers call this to

crates/oak_scan/src/watch.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ use salsa::Setter;
3636
use crate::inputs::remove_from_orphan;
3737
use crate::inputs::remove_from_pkg_files;
3838
use crate::inputs::upsert_root_file;
39-
use crate::inputs::with_appended;
40-
use crate::inputs::with_removed;
39+
use crate::inputs::with_cow_filter;
40+
use crate::inputs::with_cow_push;
4141
use crate::inputs::FileEntry;
4242
use crate::packages::classify_in_package;
4343
use crate::packages::is_r_file;
@@ -159,17 +159,17 @@ pub(crate) fn add_watched_file<DB: Db + DbInputs>(db: &mut DB, url: UrlId, conte
159159
fn append_to_container<DB: Db + DbInputs>(db: &mut DB, file: File, placement: Placement) {
160160
match placement {
161161
Placement::Script(root) => {
162-
if let Some(scripts) = with_appended(root.scripts(db), file) {
162+
if let Some(scripts) = with_cow_push(root.scripts(db), file) {
163163
root.set_scripts(db).to(scripts);
164164
}
165165
},
166166
Placement::PackageFile(pkg) => {
167-
if let Some(files) = with_appended(pkg.files(db), file) {
167+
if let Some(files) = with_cow_push(pkg.files(db), file) {
168168
pkg.set_files(db).to(files);
169169
}
170170
},
171171
Placement::PackageScript(pkg) => {
172-
if let Some(scripts) = with_appended(pkg.scripts(db), file) {
172+
if let Some(scripts) = with_cow_push(pkg.scripts(db), file) {
173173
pkg.set_scripts(db).to(scripts);
174174
}
175175
},
@@ -192,7 +192,7 @@ pub(crate) fn remove_watched_file<DB: Db + DbInputs>(db: &mut DB, url: UrlId) {
192192
}
193193

194194
for &root in &db.workspace_roots().roots(db).clone() {
195-
if let Some(scripts) = with_removed(root.scripts(db), file) {
195+
if let Some(scripts) = with_cow_filter(root.scripts(db), file) {
196196
root.set_scripts(db).to(scripts);
197197
return;
198198
}

crates/oak_scan/tests/integration/library.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashSet;
12
use std::fs;
23
use std::path::Path;
34
use std::path::PathBuf;
@@ -378,7 +379,9 @@ fn test_upsert_re_promotes_editor_owned_file_from_orphan() {
378379
// Editor opens the file before any scan -> orphan.
379380
let r_url = file_url("/lib/pkg/R/a.R");
380381
let file = File::new(&db, r_url.clone(), "editor content".to_string(), None);
381-
db.orphan_root().set_files(&mut db).to(vec![file]);
382+
db.orphan_root()
383+
.set_files(&mut db)
384+
.to(HashSet::from([file]));
382385
assert_eq!(file.package(&db), None);
383386

384387
// Now a library scan picks up the same URL as part of a package.

0 commit comments

Comments
 (0)