use super::bridge::{self, Backing, Codec};
use super::error::{CollectionError as Error, Result as CollectionResult, ensure};
use super::{Memory, Storable};
use crate::DefaultMemoryImpl;
use crate::{RuntimeGrowError, RuntimeMemory, WASM_PAGE_SIZE_BYTES as PAGE_BYTES};
use std::{marker::PhantomData, ops::Range, ops::RangeBounds, rc::Rc};
const MAX_PAGES: u64 = 4_194_304;
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum RegionGrowError {
#[error("requested region extent {minimum} exceeds {maximum} pages")]
LimitExceeded {
minimum: u64,
maximum: u64,
},
#[error("{0}")]
Runtime(#[source] RuntimeGrowError),
}
pub struct BTreeMapRegion<K: Storable + Ord + Clone, V: Storable, M: Memory = DefaultMemoryImpl> {
memory: RegionMemory<M>,
record: PhantomData<(K, V)>,
}
impl<K: Storable + Ord + Clone, V: Storable, M: Memory> BTreeMapRegion<K, V, M> {
#[must_use]
pub fn new(memory: Rc<RuntimeMemory<M>>, pages: Range<u64>) -> Self {
assert!(pages.start < pages.end && pages.end <= MAX_PAGES);
Self {
memory: RegionMemory { memory, pages },
record: PhantomData,
}
}
#[must_use]
pub fn allocated_pages(&self) -> u64 {
self.memory.size()
}
pub fn read(&self, offset: u64, dst: &mut [u8]) {
self.memory.read(offset, dst);
}
pub fn initialize(&mut self) -> CollectionResult<()> {
ensure(self.allocated_pages() == 0, Error::NotEmpty)?;
bridge::reserve(&self.memory)?;
#[cfg(not(target_arch = "wasm32"))]
let _guard = self.memory.memory.poison_on_unwind();
bridge::Map::<K, V, _>::new(Backing(&self.memory));
Ok(())
}
pub fn reserve_pages(&mut self, minimum: u64) -> Result<(), RegionGrowError> {
self.memory.reserve_pages(minimum)
}
pub fn get(&self, key: &K) -> CollectionResult<Option<V>> {
Ok(self.current_map()?.get(Codec::borrowed(key)).map(|v| v.0))
}
pub fn insert(&mut self, key: K, value: V) -> CollectionResult<Option<V>> {
#[cfg(not(target_arch = "wasm32"))]
let _guard = self.memory.memory.poison_on_unwind();
Ok(self
.current_map()?
.insert(Codec(key), Codec(value))
.map(|v| v.0))
}
pub fn remove(&mut self, key: &K) -> CollectionResult<Option<V>> {
#[cfg(not(target_arch = "wasm32"))]
let _guard = self.memory.memory.poison_on_unwind();
Ok(self
.current_map()?
.remove(Codec::borrowed(key))
.map(|v| v.0))
}
pub fn len(&self) -> CollectionResult<u64> {
Ok(self.current_map()?.len())
}
pub fn is_empty(&self) -> CollectionResult<bool> {
Ok(self.current_map()?.len() == 0)
}
pub fn keys_range(
&self,
bounds: impl RangeBounds<K>,
) -> CollectionResult<std::vec::IntoIter<K>> {
self.keys_range_limited(bounds, usize::MAX)
}
pub fn keys_range_limited(
&self,
bounds: impl RangeBounds<K>,
limit: usize,
) -> CollectionResult<std::vec::IntoIter<K>> {
if limit == 0 {
return Ok(Vec::new().into_iter());
}
Ok(self
.current_map()?
.keys_range(super::map::codec_bounds(&bounds))
.take(limit)
.map(|k| k.0)
.collect::<Vec<_>>()
.into_iter())
}
fn current_map(&self) -> CollectionResult<bridge::Map<K, V, &RegionMemory<M>>> {
bridge::Map::load(Backing(&self.memory)).ok_or(Error::Format)
}
}
struct RegionMemory<M: Memory> {
memory: Rc<RuntimeMemory<M>>,
pages: Range<u64>,
}
impl<M: Memory> RegionMemory<M> {
fn reserve_pages(&self, minimum: u64) -> Result<(), RegionGrowError> {
let maximum = self.pages.end - self.pages.start;
if minimum > maximum {
return Err(RegionGrowError::LimitExceeded { minimum, maximum });
}
if minimum > self.size() {
let additional = self.pages.start + minimum - self.memory.size();
self.memory
.as_ref()
.grow(additional)
.map_err(RegionGrowError::Runtime)?;
}
Ok(())
}
#[inline]
fn check_io(&self, offset: u64, count: usize) {
let extent = if count == 0 {
self.size()
} else {
self.pages.end - self.pages.start
} * PAGE_BYTES;
assert!(offset <= extent && count as u64 <= extent - offset);
}
}
impl<M: Memory> Memory for RegionMemory<M> {
fn size(&self) -> u64 {
self.memory.size().clamp(self.pages.start, self.pages.end) - self.pages.start
}
fn grow(&self, delta: u64) -> i64 {
let previous = self.size();
let Some(minimum) = previous.checked_add(delta) else {
return -1;
};
if self.reserve_pages(minimum).is_ok() {
i64::try_from(previous).unwrap()
} else {
-1
}
}
fn read(&self, offset: u64, dst: &mut [u8]) {
self.check_io(offset, dst.len());
if !dst.is_empty() {
self.memory
.read(self.pages.start * PAGE_BYTES + offset, dst);
}
}
fn write(&self, offset: u64, src: &[u8]) {
self.check_io(offset, src.len());
if !src.is_empty() {
self.memory
.write(self.pages.start * PAGE_BYTES + offset, src);
}
}
#[expect(
unsafe_code,
reason = "Check the region namespace before forwarding the admitted destination"
)]
unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
self.check_io(offset, count);
if count != 0 {
unsafe {
self.memory
.read_unsafe(self.pages.start * PAGE_BYTES + offset, dst, count);
}
}
}
}