diff --git a/Cargo.lock b/Cargo.lock index 38acbb00d83c..7679078e82fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,7 +1193,7 @@ dependencies = [ "indexmap", "insta", "oxc_resolver", - "papaya", + "papaya_facade", "regex", "rustc-hash 2.0.0", "schemars", @@ -2686,6 +2686,14 @@ dependencies = [ "seize", ] +[[package]] +name = "papaya_facade" +version = "0.0.0" +dependencies = [ + "papaya", + "rustc-hash 2.0.0", +] + [[package]] name = "parking_lot" version = "0.12.3" diff --git a/Cargo.toml b/Cargo.toml index e2c2ee145555..47bb9fc761f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,6 +170,7 @@ biome_migrate = { path = "./crates/biome_migrate" } biome_service = { path = "./crates/biome_service" } biome_test_utils = { path = "./crates/biome_test_utils" } biome_ungrammar = { path = "./crates/biome_ungrammar" } +papaya_facade = { path = "./crates/papaya_facade" } tests_macros = { path = "./crates/tests_macros" } # Crates needed in the workspace @@ -188,7 +189,6 @@ indexmap = { version = "2.6.0" } insta = "1.40.0" natord = "1.0.9" oxc_resolver = "1.12.0" -papaya = "0.1.4" proc-macro2 = "1.0.86" quickcheck = "1.0.3" quickcheck_macros = "1.0.0" diff --git a/crates/biome_service/Cargo.toml b/crates/biome_service/Cargo.toml index 9ea65f8753fb..3776eedd8dbe 100644 --- a/crates/biome_service/Cargo.toml +++ b/crates/biome_service/Cargo.toml @@ -59,7 +59,7 @@ getrandom = { workspace = true, features = ["js"] } ignore = { workspace = true } indexmap = { workspace = true, features = ["serde"] } oxc_resolver = { workspace = true } -papaya = { workspace = true } +papaya_facade = { workspace = true } regex = { workspace = true } rustc-hash = { workspace = true } schemars = { workspace = true, optional = true } diff --git a/crates/biome_service/src/workspace/server.rs b/crates/biome_service/src/workspace/server.rs index 37ed726fce6e..cae303d4be72 100644 --- a/crates/biome_service/src/workspace/server.rs +++ b/crates/biome_service/src/workspace/server.rs @@ -34,7 +34,7 @@ use biome_parser::AnyParse; use biome_project::{NodeJsProject, PackageJson, PackageType, Project}; use biome_rowan::NodeCache; use indexmap::IndexSet; -use papaya::HashMap; +use papaya_facade::HashMap; use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/papaya_facade/Cargo.toml b/crates/papaya_facade/Cargo.toml new file mode 100644 index 000000000000..55ffece8a315 --- /dev/null +++ b/crates/papaya_facade/Cargo.toml @@ -0,0 +1,21 @@ + +[package] +authors.workspace = true +categories.workspace = true +description = "Facade for papaya with a WASM-compatible shim" +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +name = "papaya_facade" +repository.workspace = true +version = "0.0.0" + +[lints] +workspace = true + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +papaya = "0.1.4" + +[target.'cfg(target_arch = "wasm32")'.dependencies] +rustc-hash = { workspace = true } diff --git a/crates/papaya_facade/src/lib.rs b/crates/papaya_facade/src/lib.rs new file mode 100644 index 000000000000..b1bfc462234a --- /dev/null +++ b/crates/papaya_facade/src/lib.rs @@ -0,0 +1,9 @@ +#[cfg(not(target_arch = "wasm32"))] +mod papaya; +#[cfg(target_arch = "wasm32")] +mod shim; + +#[cfg(not(target_arch = "wasm32"))] +pub use crate::papaya::HashMap; +#[cfg(target_arch = "wasm32")] +pub use shim::HashMap; diff --git a/crates/papaya_facade/src/papaya.rs b/crates/papaya_facade/src/papaya.rs new file mode 100644 index 000000000000..1da916004fc6 --- /dev/null +++ b/crates/papaya_facade/src/papaya.rs @@ -0,0 +1 @@ +pub use ::papaya::HashMap; diff --git a/crates/papaya_facade/src/shim.rs b/crates/papaya_facade/src/shim.rs new file mode 100644 index 000000000000..a206d1c37c64 --- /dev/null +++ b/crates/papaya_facade/src/shim.rs @@ -0,0 +1,305 @@ +use std::{borrow::Borrow, cell::UnsafeCell, collections::hash_map, fmt, hash::Hash}; + +use rustc_hash::{FxBuildHasher, FxHashMap}; + +/// Provides a shim with the same API as `papaya`, but which is fundamentally +/// single-threaded and works in a WASM environment. +/// +/// SAFETY: This shim is *only* safe in a single-threaded WASM environment. +/// Concurrent access to this hash map may lead to undefined behavior. +#[derive(Debug)] +pub struct HashMap { + inner: UnsafeCell>, +} + +// SAFETY: This is only intended for single-threaded WASM environments. +unsafe impl Sync for HashMap {} + +impl Default for HashMap { + fn default() -> Self { + Self { + inner: UnsafeCell::default(), + } + } +} + +impl HashMap { + pub fn new() -> HashMap { + HashMap::with_capacity(0) + } + + pub fn with_capacity(capacity: usize) -> Self { + Self { + inner: UnsafeCell::new(FxHashMap::with_capacity_and_hasher(capacity, FxBuildHasher)), + } + } + + #[inline] + pub fn pin(&self) -> HashMapRef<'_, K, V> { + HashMapRef { + guard: self.guard(), + map: self, + } + } + + #[inline] + pub fn pin_owned(&self) -> HashMapRef<'_, K, V> { + self.pin() + } + + #[inline] + pub fn guard(&self) -> Guard { + Guard { + removed_values: Default::default(), + } + } + + #[inline] + pub fn owned_guard(&self) -> Guard { + self.guard() + } + + pub fn len(&self) -> usize { + self.raw().len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// SAFETY: No mutable references to the hash map may exist at the same time. + fn raw(&self) -> &FxHashMap { + unsafe { &*self.inner.get() } + } + + /// SAFETY: No other references to the hash map may exist at the same time. + #[allow(clippy::mut_from_ref)] + fn raw_mut(&self) -> &mut FxHashMap { + unsafe { &mut *self.inner.get() } + } +} + +pub struct Guard { + removed_values: UnsafeCell>, +} + +impl Guard { + /// SAFETY: No mutable references to the vector may exist at the same time. + fn raw_removed_values(&self) -> &Vec { + unsafe { &*self.removed_values.get() } + } + + /// SAFETY: No other references to the vector may exist at the same time. + #[allow(clippy::mut_from_ref)] + fn raw_removed_values_mut(&self) -> &mut Vec { + unsafe { &mut *self.removed_values.get() } + } +} + +pub struct HashMapRef<'map, K, V> { + guard: Guard, + map: &'map HashMap, +} + +impl<'map, K, V> HashMapRef<'map, K, V> +where + K: Hash + Eq, +{ + #[inline] + pub fn map(&self) -> &'map HashMap { + self.map + } + + #[inline] + pub fn len(&self) -> usize { + self.map.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + #[inline] + pub fn contains_key(&self, key: &Q) -> bool + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + self.get(key).is_some() + } + + #[inline] + pub fn get(&self, key: &Q) -> Option<&V> + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + self.map.raw().get(key) + } + + #[inline] + pub fn get_key_value(&self, key: &Q) -> Option<(&K, &V)> + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + self.map.raw().get_key_value(key) + } + + #[inline] + pub fn insert(&self, key: K, value: V) -> Option<&V> { + match self.map.raw_mut().insert(key, value) { + Some(old_value) => { + self.guard.raw_removed_values_mut().push(old_value); + self.guard.raw_removed_values().last() + } + None => None, + } + } + + #[inline] + pub fn try_insert(&self, key: K, value: V) -> Result<&V, OccupiedError<'_, V>> + where + K: Clone, + { + if let Some(current) = self.map.raw().get(&key) { + return Err(OccupiedError { + current, + not_inserted: value, + }); + } + + self.map.raw_mut().insert(key.clone(), value); + self.map.raw().get(&key).ok_or_else(|| unreachable!()) + } + + #[inline] + pub fn get_or_insert(&self, key: K, value: V) -> &V + where + K: Clone, + { + // Note that we use `try_insert` instead of `compute` or `get_or_insert_with` here, as it + // allows us to avoid the closure indirection. + match self.try_insert(key, value) { + Ok(inserted) => inserted, + Err(OccupiedError { current, .. }) => current, + } + } + + #[inline] + pub fn get_or_insert_with(&self, key: K, f: F) -> &V + where + F: FnOnce() -> V, + K: Clone, + { + if let Some(current) = self.map.raw().get(&key) { + return current; + } + + self.map.raw_mut().insert(key.clone(), f()); + self.map.raw().get(&key).unwrap() + } + + #[inline] + pub fn update(&self, key: K, update: F) -> Option<&V> + where + F: Fn(&V) -> V, + K: Clone, + { + self.map.raw().get(&key).and_then(|current| { + self.map.raw_mut().insert(key.clone(), update(current)); + self.map.raw().get(&key) + }) + } + + #[inline] + pub fn update_or_insert(&self, key: K, update: F, value: V) -> &V + where + F: Fn(&V) -> V, + K: Clone, + { + self.update_or_insert_with(key, update, || value) + } + + #[inline] + pub fn update_or_insert_with(&self, key: K, update: U, f: F) -> &V + where + F: FnOnce() -> V, + K: Clone, + U: Fn(&V) -> V, + { + let value = match self.map.raw().get(&key) { + Some(current) => update(current), + None => f(), + }; + + self.map.raw_mut().insert(key.clone(), value); + self.map.raw().get(&key).unwrap() + } + + #[inline] + pub fn remove(&self, key: &Q) -> Option<&V> + where + K: Borrow, + Q: Hash + Eq + ?Sized, + { + match self.map.raw_mut().remove(key) { + Some(old_value) => { + self.guard.raw_removed_values_mut().push(old_value); + self.guard.raw_removed_values().last() + } + None => None, + } + } + + #[inline] + pub fn clear(&self) { + self.map().raw_mut().clear() + } + + #[inline] + pub fn reserve(&self, additional: usize) { + self.map().raw_mut().reserve(additional) + } + + #[inline] + pub fn iter(&self) -> Iter<'_, K, V> { + Iter { + inner: self.map().raw().iter(), + } + } +} + +pub struct OccupiedError<'a, V: 'a> { + pub current: &'a V, + pub not_inserted: V, +} + +pub struct Iter<'g, K, V> { + inner: hash_map::Iter<'g, K, V>, +} + +impl<'g, K: 'g, V: 'g> Iterator for Iter<'g, K, V> { + type Item = (&'g K, &'g V); + + #[inline] + fn next(&mut self) -> Option { + self.inner.next() + } +} + +impl fmt::Debug for Iter<'_, K, V> +where + K: fmt::Debug, + V: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list() + .entries(Iter { + inner: self.inner.clone(), + }) + .finish() + } +} diff --git a/knope.toml b/knope.toml index 79a5d6ff5f5c..3badcd6c952e 100644 --- a/knope.toml +++ b/knope.toml @@ -222,6 +222,10 @@ versioned_files = ["crates/biome_css_semantic/Cargo.toml"] changelog = "crates/biome_plugin_loader/CHANGELOG.md" versioned_files = ["crates/biome_plugin_loader/Cargo.toml"] +[packages.papaya_facade] +changelog = "crates/papaya_facade/CHANGELOG.md" +versioned_files = ["crates/papaya_facade/Cargo.toml"] + ## End of crates. DO NOT CHANGE! # Workflow to create a changeset