Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add borrowed-only version of ZeroMap #1238

Merged
merged 20 commits into from
Nov 9, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions utils/zerovec/src/map/borrowed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use crate::ule::AsULE;
use crate::ZeroVec;

pub use super::kv::ZeroMapKV;
pub use super::vecs::{BorrowedZeroVecLike, MutableZeroVecLike, ZeroVecLike};

/// A borrowed-only version of [`ZeroMap`](super::ZeroMap)
///
/// This is useful for fully-zero-copy deserialization from non-human-readable
/// serialization formats. It also has the advantage that it can return references that live for
/// the lifetime of the backing buffer as opposed to that of the [`ZeroMapBorrowed`] instance.
///
/// # Examples
///
/// ```
/// use zerovec::map::ZeroMapBorrowed;
///
/// // Example byte buffer representing the map { 1: "one" }
/// let BINCODE_BYTES: &[u8; 31] = &[
/// 4, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0,
/// 1, 0, 0, 0, 0, 0, 0, 0, 111, 110, 101
/// ];
///
/// // Deserializing to ZeroMap requires no heap allocations.
/// let zero_map: ZeroMapBorrowed<u32, str> = bincode::deserialize(BINCODE_BYTES)
/// .expect("Should deserialize successfully");
/// assert_eq!(zero_map.get(&1), Some("one"));
/// ```
///
/// This can be obtained from a [`ZeroMap`](super::ZeroMap) via [`ZeroMap::as_borrowed`](super::ZeroMap::as_borrowed)
pub struct ZeroMapBorrowed<'a, K, V>
where
K: ZeroMapKV<'a>,
V: ZeroMapKV<'a>,
K: ?Sized,
V: ?Sized,
{
pub(crate) keys:
<<K as ZeroMapKV<'a>>::Container as MutableZeroVecLike<'a, K>>::BorrowedVariant,
pub(crate) values:
<<V as ZeroMapKV<'a>>::Container as MutableZeroVecLike<'a, V>>::BorrowedVariant,
}

impl<'a, K, V> Copy for ZeroMapBorrowed<'a, K, V>
where
K: ZeroMapKV<'a>,
V: ZeroMapKV<'a>,
K: ?Sized,
V: ?Sized,
{
}
impl<'a, K, V> Clone for ZeroMapBorrowed<'a, K, V>
where
K: ZeroMapKV<'a>,
V: ZeroMapKV<'a>,
K: ?Sized,
V: ?Sized,
{
fn clone(&self) -> Self {
ZeroMapBorrowed {
keys: self.keys,
values: self.values,
}
}
}

impl<'a, K, V> ZeroMapBorrowed<'a, K, V>
where
K: ZeroMapKV<'a>,
V: ZeroMapKV<'a>,
K: ?Sized,
V: ?Sized,
{
/// The number of elements in the [`ZeroMapBorrowed`]
pub fn len(&self) -> usize {
self.values.len()
}

/// Whether the [`ZeroMapBorrowed`] is empty
pub fn is_empty(&self) -> bool {
self.values.len() == 0
}

/// Get the value associated with `key`, if it exists.
///
/// This is able to return values that live longer than the map itself
/// since they borrow directly from the backing buffer. This is the
/// primary advantage of using [`ZeroMapBorrowed`] over [`ZeroMap`].
///
/// ```rust
/// use zerovec::ZeroMap;
/// use zerovec::map::ZeroMapBorrowed;
///
/// let mut map = ZeroMap::new();
/// map.insert(&1, "one");
/// map.insert(&2, "two");
/// let borrowed = map.as_borrowed();
/// assert_eq!(borrowed.get(&1), Some("one"));
/// assert_eq!(borrowed.get(&3), None);
///
/// let borrow = borrowed.get(&1);
/// drop(borrowed);
/// // still exists after the ZeroMapBorrowed has been dropped
/// assert_eq!(borrow, Some("one"));
/// ```
pub fn get(&self, key: &K::NeedleType) -> Option<&'a V::GetType> {
let index = self.keys.binary_search(key).ok()?;
self.values.get_lengthened(index)
}

/// Returns whether `key` is contained in this map
///
/// ```rust
/// use zerovec::ZeroMap;
/// use zerovec::map::ZeroMapBorrowed;
///
/// let mut map = ZeroMap::new();
/// map.insert(&1, "one");
/// map.insert(&2, "two");
/// let borrowed = map.as_borrowed();
/// assert_eq!(borrowed.contains_key(&1), true);
/// assert_eq!(borrowed.contains_key(&3), false);
/// ```
pub fn contains_key(&self, key: &K::NeedleType) -> bool {
self.keys.binary_search(key).is_ok()
}

/// Produce an ordered iterator over key-value pairs
pub fn iter<'b>(
&'b self,
) -> impl Iterator<
Item = (
&'a <K as ZeroMapKV<'a>>::GetType,
&'a <V as ZeroMapKV<'a>>::GetType,
),
> + 'b {
(0..self.keys.len()).map(move |idx| {
(
self.keys.get_lengthened(idx).unwrap(),
self.values.get_lengthened(idx).unwrap(),
)
})
}

/// Produce an ordered iterator over keys
pub fn iter_keys<'b>(&'b self) -> impl Iterator<Item = &'a <K as ZeroMapKV<'a>>::GetType> + 'b {
(0..self.keys.len()).map(move |idx| self.keys.get_lengthened(idx).unwrap())
}

/// Produce an iterator over values, ordered by keys
pub fn iter_values<'b>(
&'b self,
) -> impl Iterator<Item = &'a <V as ZeroMapKV<'a>>::GetType> + 'b {
(0..self.values.len()).map(move |idx| self.values.get_lengthened(idx).unwrap())
}
}

impl<'a, K, V> ZeroMapBorrowed<'a, K, V>
where
K: ZeroMapKV<'a>,
V: ZeroMapKV<'a, Container = ZeroVec<'a, V>>,
V: AsULE + Ord + Copy,
{
/// For cases when `V` is fixed-size, obtain a direct copy of `V` instead of `V::ULE`
pub fn get_copied(&self, key: &K::NeedleType) -> Option<V> {
let index = self.keys.binary_search(key).ok()?;
<[V::ULE]>::get(self.values, index)
.copied()
.map(V::from_unaligned)
}

/// Similar to [`Self::iter()`] except it returns a direct copy of the values instead of references
/// to `V::ULE`, in cases when `V` is fixed-size
pub fn iter_copied_values<'b>(
&'b self,
) -> impl Iterator<Item = (&'b <K as ZeroMapKV<'a>>::GetType, V)> {
(0..self.keys.len()).map(move |idx| {
(
self.keys.get(idx).unwrap(),
<[V::ULE]>::get(self.values, idx)
.copied()
.map(V::from_unaligned)
.unwrap(),
)
})
}
}

impl<'a, K, V> ZeroMapBorrowed<'a, K, V>
where
K: ZeroMapKV<'a, Container = ZeroVec<'a, K>>,
V: ZeroMapKV<'a, Container = ZeroVec<'a, V>>,
K: AsULE + Copy + Ord,
V: AsULE + Copy + Ord,
{
/// Similar to [`Self::iter()`] except it returns a direct copy of the keys values instead of references
/// to `K::ULE` and `V::ULE`, in cases when `K` and `V` are fixed-size
#[allow(clippy::needless_lifetimes)] // Lifetime is necessary in impl Trait
pub fn iter_copied<'b>(&'b self) -> impl Iterator<Item = (K, V)> + 'b {
let keys = &self.keys;
let values = &self.values;
let len = <[K::ULE]>::len(keys);
(0..len).map(move |idx| {
(
<[K::ULE]>::get(keys, idx)
.copied()
.map(K::from_unaligned)
.unwrap(),
<[V::ULE]>::get(values, idx)
.copied()
.map(V::from_unaligned)
.unwrap(),
)
})
}
}
6 changes: 3 additions & 3 deletions utils/zerovec/src/map/kv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use super::vecs::ZeroVecLike;
use super::vecs::MutableZeroVecLike;
use crate::ule::*;
use crate::VarZeroVec;
use crate::ZeroVec;
Expand All @@ -18,7 +18,7 @@ use core::cmp::Ordering;
#[allow(clippy::upper_case_acronyms)] // KV is not an acronym
pub trait ZeroMapKV<'a> {
/// The container that can be used with this type: [`ZeroVec`] or [`VarZeroVec`].
type Container: ZeroVecLike<
type Container: MutableZeroVecLike<
'a,
Self,
NeedleType = Self::NeedleType,
Expand All @@ -32,7 +32,7 @@ pub trait ZeroMapKV<'a> {
/// The type produced by `Container::get()`
///
/// This type will be predetermined by the choice of `Self::Container`
type GetType: ?Sized;
type GetType: ?Sized + 'static;
/// The type to use whilst serializing. This may not necessarily be `Self`, however it
/// must serialize to the exact same thing as `Self`
type SerializeType: ?Sized;
Expand Down
27 changes: 26 additions & 1 deletion utils/zerovec/src/map/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ use crate::ule::AsULE;
use crate::ZeroVec;
use core::cmp::Ordering;

mod borrowed;
mod kv;
#[cfg(feature = "serde")]
mod serde;
mod vecs;

pub use borrowed::ZeroMapBorrowed;
pub use kv::ZeroMapKV;
pub use vecs::ZeroVecLike;
pub use vecs::{MutableZeroVecLike, ZeroVecLike};

/// A zero-copy map datastructure, built on sorted binary-searchable [`ZeroVec`]
/// and [`VarZeroVec`].
Expand Down Expand Up @@ -91,6 +93,14 @@ where
}
}

/// Obtain a borrowed version of this map
pub fn as_borrowed(&'a self) -> ZeroMapBorrowed<'a, K, V> {
ZeroMapBorrowed {
keys: self.keys.as_borrowed(),
values: self.values.as_borrowed(),
}
}

/// The number of elements in the [`ZeroMap`]
pub fn len(&self) -> usize {
self.values.len()
Expand Down Expand Up @@ -297,3 +307,18 @@ where
})
}
}

impl<'a, K, V> From<ZeroMapBorrowed<'a, K, V>> for ZeroMap<'a, K, V>
where
K: ZeroMapKV<'a>,
V: ZeroMapKV<'a>,
K: ?Sized,
V: ?Sized,
{
fn from(other: ZeroMapBorrowed<'a, K, V>) -> Self {
Self {
keys: K::Container::from_borrowed(other.keys),
values: V::Container::from_borrowed(other.values),
}
}
}
69 changes: 68 additions & 1 deletion utils/zerovec/src/map/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

use super::{ZeroMap, ZeroMapKV, ZeroVecLike};
use super::{MutableZeroVecLike, ZeroMap, ZeroMapBorrowed, ZeroMapKV, ZeroVecLike};
use alloc::boxed::Box;
use core::fmt;
use core::marker::PhantomData;
Expand Down Expand Up @@ -39,6 +39,26 @@ where
}
}

/// This impl can be made available by enabling the optional `serde` feature of the `zerovec` crate
impl<'a, K, V> Serialize for ZeroMapBorrowed<'a, K, V>
where
K: ZeroMapKV<'a>,
V: ZeroMapKV<'a>,
K: ?Sized,
V: ?Sized,
K::Container: Serialize,
V::Container: Serialize,
K::SerializeType: Serialize,
V::SerializeType: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
ZeroMap::<K, V>::from(*self).serialize(serializer)
}
}

/// Modified example from https://serde.rs/deserialize-map.html
struct ZeroMapMapVisitor<K: ?Sized, V: ?Sized> {
#[allow(clippy::type_complexity)] // it's a marker type, complexity doesn't matter
Expand Down Expand Up @@ -126,6 +146,46 @@ where
}
}

// /// This impl can be made available by enabling the optional `serde` feature of the `zerovec` crate
impl<'de, K: ?Sized, V: ?Sized> Deserialize<'de> for ZeroMapBorrowed<'de, K, V>
where
K: Ord,
K::Container: Deserialize<'de>,
V::Container: Deserialize<'de>,
K: ZeroMapKV<'de>,
V: ZeroMapKV<'de>,
K::OwnedType: Deserialize<'de>,
V::OwnedType: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
if deserializer.is_human_readable() {
Err(de::Error::custom(
"ZeroMapBorrowed cannot be deserialized from human-readable formats",
))
} else {
let deserialized: ZeroMap<'de, K, V> = ZeroMap::deserialize(deserializer)?;
let keys = if let Some(keys) = deserialized.keys.as_borrowed_inner() {
keys
} else {
return Err(de::Error::custom(
"ZeroMapBorrowed can only deserialize in zero-copy ways",
));
};
let values = if let Some(values) = deserialized.values.as_borrowed_inner() {
values
} else {
return Err(de::Error::custom(
"ZeroMapBorrowed can only deserialize in zero-copy ways",
));
};
Ok(Self { keys, values })
}
}
}

#[cfg(test)]
mod test {
use super::super::*;
Expand Down Expand Up @@ -165,5 +225,12 @@ mod test {
new_map.iter().collect::<Vec<_>>(),
map.iter().collect::<Vec<_>>()
);

let new_map: ZeroMapBorrowed<u32, str> =
bincode::deserialize(&bincode_bytes).expect("deserialize");
assert_eq!(
new_map.iter().collect::<Vec<_>>(),
map.iter().collect::<Vec<_>>()
);
}
}
Loading