wasm/execution/runtime_structure/memory_instances/
linear_memory.rs

1use alloc::vec::Vec;
2use core::{
3    iter,
4    sync::atomic::{AtomicU8, Ordering},
5};
6
7use crate::{
8    execution::numerics::representations::LittleEndianBytes,
9    rw_spinlock::{ReadLockGuard, RwSpinLock},
10    RuntimeError, TrapError,
11};
12
13/// Implementation of the linear memory suitable for concurrent access
14///
15/// Implements the base for the instructions described in
16/// <https://webassembly.github.io/spec/core/exec/instructions.html#memory-instructions>.
17///
18/// This linear memory implementation internally relies on a [`Vec<AtomicU8>`]. Thus, the atomic unit
19/// of information for it is a byte (`u8`). All access to the linear memory internally occur through
20/// [`AtomicU8::load`] and [`AtomicU8::store`], avoiding the creation of shared and `mut ref`s to
21/// the internal data completely. This avoids undefined behavior. Racy multibyte writes to the same
22/// data however may tear (e.g. for any number of concurrent writes to a given byte, only one is
23/// effectively written). Because of this, the [`LinearMemory::store`] function does not require
24/// `&mut self` -- `&self` suffices.
25///
26/// The implementation of atomic stores to multibyte values requires a global write lock. Rust's
27/// memory model considers partially overlapping atomic operations involving a write as undefined
28/// behavior. As there is no way to predict if an atomic multibyte store operation might overlap
29/// with another store or load operation, only a lock at runtime can avoid this cause of undefined
30/// behavior.
31// TODO does it pay of to have more fine-granular locking for multibyte stores than a single global write lock?
32///
33/// # Notes on overflowing
34///
35/// All operations that rely on accessing `n` bytes starting at `index` in the linear memory have to
36/// perform bounds checking. Thus, they always have to ensure that `n + index < linear_memory.len()`
37/// holds true (e.g. `n + index - 1` must be a valid index into `linear_memory`). However,
38/// writing that check as is bears the danger of an overflow, assuming that `n`, `index` and
39/// `linear_memory.len()` are the same given integer type, `n + index` can overflow, resulting in
40/// the check passing despite the access being out of bounds!
41///
42/// To avoid this, the bounds checks are carefully ordered to avoid any overflows:
43///
44/// - First we check, that `n <= linear_memory.len()` holds true, ensuring that the amount of bytes
45///   to be accessed is indeed smaller than or equal to the linear memory's size. If this does not
46///   hold true, continuation of the operation will yield out of bounds access in any case.
47/// - Then, as a second check, we verify that `index <= linear_memory.len() - n`. This way we
48///   avoid the overflow, as there is no addition. The subtraction in the left hand can not
49///   underflow, due to the previous check (which asserts that `n` is smaller than or equal to
50///   `linear_memory.len()`).
51///
52/// Combined in the given order, these two checks enable bounds checking without risking any
53/// overflow or underflow, provided that `n`, `index` and `linear_memory.len()` are of the same
54/// integer type.
55///
56/// In addition, the Wasm specification requires a certain order of checks. For example, when a
57/// `copy` instruction is emitted with a `count` of zero (i.e. no bytes to be copied), an out of
58/// bounds index still has to cause a trap. To control the order of checks manually, use of slice
59/// indexing is avoided altogether.
60///
61/// # Notes on locking
62///
63/// The internal data vector of the [`LinearMemory`] is wrapped in a [`RwSpinLock`]. Despite the
64/// name, writes to the linear memory do not require an acquisition of a write lock. Non-atomic
65/// or atomic single-byte writes are implemented through a shared ref to the internal vector, with
66/// [`AtomicU8`] to achieve interior mutability without undefined behavior.
67///
68/// However, linear memory can grow. As the linear memory is implemented via a [`Vec`], a `grow`
69/// can result in the vector's internal data buffer to be copied over to a bigger, fresh allocation.
70/// The old buffer is then freed. Combined with concurrent access, this can cause use-after-free.
71/// To avoid this, a `grow` operation of the linear memory acquires a write lock, blocking all
72/// read/write to the linear memory in between.
73///
74/// # Unsafe Note
75///
76/// As the manual index checking assures all indices to be valid, there is no need to re-check.
77/// Therefore [`slice::get_unchecked`] is used access the internal [`AtomicU8`] in the vector
78/// backing a [`LinearMemory`], implicating the use of `unsafe`.
79///
80/// To gain some confidence in the correctness of the unsafe code in this module, run `miri`:
81///
82/// ```bash
83/// cargo miri test --test memory # quick
84/// cargo miri test # thorough
85/// ```
86// TODO if a memmap like operation is available, the linear memory implementation can be optimized brutally. Out-of-bound access can be mapped to userspace handled page-faults, e.g. the MMU takes over that responsibility of catching out of bounds. Grow can happen without copying of data, by mapping new pages consecutively after the current final page of the linear memory.
87pub struct LinearMemory<const PAGE_SIZE: usize = { crate::Limits::MEM_PAGE_SIZE as usize }> {
88    inner_data: RwSpinLock<Vec<AtomicU8>>,
89}
90
91/// Type to express the page count
92pub type PageCountTy = u16;
93
94impl<const PAGE_SIZE: usize> LinearMemory<PAGE_SIZE> {
95    /// Size of a page in the linear memory, measured in bytes
96    ///
97    /// The WASM specification demands a page size of 64 KiB, that is `65536` bytes:
98    /// <https://webassembly.github.io/spec/core/exec/runtime.html?highlight=page#memory-instances>
99    const PAGE_SIZE: usize = PAGE_SIZE;
100
101    /// Create a new, empty [`LinearMemory`]
102    pub fn new() -> Self {
103        Self {
104            inner_data: RwSpinLock::new(Vec::new()),
105        }
106    }
107
108    /// Create a new, empty [`LinearMemory`]
109    pub fn new_with_initial_pages(pages: PageCountTy) -> Self {
110        let size_bytes = Self::PAGE_SIZE * usize::from(pages);
111        let mut data = Vec::with_capacity(size_bytes);
112        data.resize_with(size_bytes, || AtomicU8::new(0));
113
114        Self {
115            inner_data: RwSpinLock::new(data),
116        }
117    }
118
119    /// Grow the [`LinearMemory`] by a number of pages
120    pub fn grow(&self, pages_to_add: PageCountTy) {
121        let mut lock_guard = self.inner_data.write();
122        let prior_length_bytes = lock_guard.len();
123        let new_length_bytes = prior_length_bytes + Self::PAGE_SIZE * usize::from(pages_to_add);
124        lock_guard.resize_with(new_length_bytes, || AtomicU8::new(0));
125    }
126
127    /// Get the number of pages currently allocated to this [`LinearMemory`]
128    pub fn pages(&self) -> PageCountTy {
129        PageCountTy::try_from(self.inner_data.read().len() / PAGE_SIZE).unwrap()
130    }
131
132    /// Get the length in bytes currently allocated to this [`LinearMemory`]
133    // TODO remove this op
134    pub fn len(&self) -> usize {
135        self.inner_data.read().len()
136    }
137
138    /// At a given index, store a datum in the [`LinearMemory`]
139    pub fn store<const N: usize, T: LittleEndianBytes<N>>(
140        &self,
141        index: usize,
142        value: T,
143    ) -> Result<(), RuntimeError> {
144        self.store_bytes::<N>(index, value.to_le_bytes())
145    }
146
147    /// At a given index, store a number of bytes `N` in the [`LinearMemory`]
148    pub fn store_bytes<const N: usize>(
149        &self,
150        index: usize,
151        bytes: [u8; N],
152    ) -> Result<(), RuntimeError> {
153        let lock_guard = self.inner_data.read();
154
155        /* check destination for out of bounds access */
156        // A value must fit into the linear memory
157        if N > lock_guard.len() {
158            error!("value does not fit into linear memory");
159            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
160        }
161
162        // The following statement must be true
163        // `index + N <= lock_guard.len()`
164        // This check verifies it, while avoiding the possible overflow. The subtraction can not
165        // underflow because of the previous check.
166
167        if index > lock_guard.len() - N {
168            error!("value write would extend beyond the end of the linear memory");
169            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
170        }
171
172        /* do the store */
173        for (i, byte) in bytes.into_iter().enumerate() {
174            // SAFETY:
175            // The safety of this `unsafe` block depends on the index being valid, which it is
176            // because:
177            //
178            // - the first if statement in this function guarantees that a `T` can fit into the
179            //   `LinearMemory` `&self`
180            // - the second if statement in this function guarantees that even with the offset
181            //   `index`, writing all of `value`'s bytes does not extend beyond the last byte in
182            //   the `LinearMemory` `&self`
183            let dst = unsafe { lock_guard.get_unchecked(i + index) };
184            dst.store(byte, Ordering::Relaxed);
185        }
186
187        Ok(())
188    }
189
190    /// From a given index, load a datum from the [`LinearMemory`]
191    pub fn load<const N: usize, T: LittleEndianBytes<N>>(
192        &self,
193        index: usize,
194    ) -> Result<T, RuntimeError> {
195        self.load_bytes::<N>(index).map(T::from_le_bytes)
196    }
197
198    /// From a given index, load a number of bytes `N` from the [`LinearMemory`]
199    pub fn load_bytes<const N: usize>(&self, index: usize) -> Result<[u8; N], RuntimeError> {
200        let lock_guard = self.inner_data.read();
201
202        /* check source for out of bounds access */
203        // A value must fit into the linear memory
204        if N > lock_guard.len() {
205            error!("value does not fit into linear memory");
206            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
207        }
208
209        // The following statement must be true
210        // `index + N <= lock_guard.len()`
211        // This check verifies it, while avoiding the possible overflow. The subtraction can not
212        // underflow because of the previous assert.
213
214        if index > lock_guard.len() - N {
215            error!("value read would extend beyond the end of the linear_memory");
216            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
217        }
218
219        let mut bytes = [0; N];
220
221        /* do the load */
222        for (i, byte) in bytes.iter_mut().enumerate() {
223            // SAFETY:
224            // The safety of this `unsafe` block depends on the index being valid, which it is
225            // because:
226            //
227            // - the first if statement in this function guarantees that a `T` can fit into the
228            //   `LinearMemory` `&self`
229            // - the second if statement in this function guarantees that even with the offset
230            //   `index`, reading all `N` bytes does not extend beyond the last byte in
231            //   the `LinearMemory` `&self`
232            let src = unsafe { lock_guard.get_unchecked(i + index) };
233            *byte = src.load(Ordering::Relaxed);
234        }
235
236        Ok(bytes)
237    }
238
239    /// Implementation of the behavior described in
240    /// <https://webassembly.github.io/spec/core/exec/instructions.html#xref-syntax-instructions-syntax-instr-memory-mathsf-memory-fill>.
241    /// Note, that the WASM spec defines the behavior by recursion, while our implementation uses
242    /// the memset like [`core::ptr::write_bytes`].
243    ///
244    /// <https://webassembly.github.io/spec/core/exec/instructions.html#xref-syntax-instructions-syntax-instr-memory-mathsf-memory-fill>
245    pub fn fill(&self, index: usize, data_byte: u8, count: usize) -> Result<(), RuntimeError> {
246        let lock_guard = self.inner_data.read();
247
248        /* check destination for out of bounds access */
249        // Specification step 12.
250        if count > lock_guard.len() {
251            error!("fill count is bigger than the linear memory");
252            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
253        }
254
255        // Specification step 12.
256        if index > lock_guard.len() - count {
257            error!("fill extends beyond the linear memory's end");
258            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
259        }
260
261        /* check if there is anything to be done */
262        // Specification step 13.
263        if count == 0 {
264            return Ok(());
265        }
266
267        /* do the fill */
268        // Specification step 14-21.
269        for i in index..(index + count) {
270            // SAFETY:
271            // The safety of this `unsafe` block depends on the index being valid, which it is
272            // because:
273            //
274            // - the first if statement in this function guarantees that `count` elements can fit
275            //   into the `LinearMemory` `&self`
276            // - the second if statement in this function guarantees that even with the offset
277            //   `index`, writing all `count`'s bytes does not extend beyond the last byte in
278            //   the `LinearMemory` `&self`
279            let lin_mem_byte = unsafe { lock_guard.get_unchecked(i) };
280            lin_mem_byte.store(data_byte, Ordering::Relaxed);
281        }
282
283        Ok(())
284    }
285
286    /// Copy `count` bytes from one region in the linear memory to another region in the same or a
287    /// different linear memory
288    ///
289    /// - Both regions may overlap
290    /// - Copies the `count` bytes starting from `source_index`, overwriting the `count` bytes
291    ///   starting from `destination_index`
292    ///
293    /// <https://webassembly.github.io/spec/core/exec/instructions.html#xref-syntax-instructions-syntax-instr-memory-mathsf-memory-copy>
294    pub fn copy(
295        &self,
296        destination_index: usize,
297        source_mem: &Self,
298        source_index: usize,
299        count: usize,
300    ) -> Result<(), RuntimeError> {
301        // self is the destination
302        let lock_guard_self = self.inner_data.read();
303
304        // other is the source
305        let lock_guard_other = source_mem.inner_data.read();
306
307        /* check source for out of bounds access */
308        // Specification step 12.
309        if count > lock_guard_other.len() {
310            error!("copy count is bigger than the source linear memory");
311            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
312        }
313
314        // Specification step 12.
315        if source_index > lock_guard_other.len() - count {
316            error!("copy source extends beyond the linear memory's end");
317            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
318        }
319
320        /* check destination for out of bounds access */
321        // Specification step 12.
322        if count > lock_guard_self.len() {
323            error!("copy count is bigger than the destination linear memory");
324            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
325        }
326
327        // Specification step 12.
328        if destination_index > lock_guard_self.len() - count {
329            error!("copy destination extends beyond the linear memory's end");
330            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
331        }
332
333        /* check if there is anything to be done */
334        // Specification step 13.
335        if count == 0 {
336            return Ok(());
337        }
338
339        /* do the copy */
340        let copy_one_byte = move |i| {
341            // SAFETY:
342            // The safety of this `unsafe` block depends on the index being valid, which it is
343            // because:
344            //
345            // - the first if statement in this function guarantees that `count` elements can fit
346            //   into the `LinearMemory` `&source_mem`
347            // - the second if statement in this function guarantees that even with the offset
348            //   `source_index`, writing all `count`'s bytes does not extend beyond the last byte in
349            let src_byte: &AtomicU8 = unsafe { lock_guard_other.get_unchecked(i + source_index) };
350
351            // SAFETY:
352            // The safety of this `unsafe` block depends on the index being valid, which it is
353            // because:
354            //
355            // - the third if statement in this function guarantees that `count` elements can fit
356            //   into the `LinearMemory` `&self`
357            // - the fourth if statement in this function guarantees that even with the offset
358            //   `destination_index`, writing all `count`'s bytes does not extend beyond the last byte in
359            //   the `LinearMemory` `&self`
360            let dst_byte: &AtomicU8 =
361                unsafe { lock_guard_self.get_unchecked(i + destination_index) };
362
363            let byte = src_byte.load(Ordering::Relaxed);
364            dst_byte.store(byte, Ordering::Relaxed);
365        };
366
367        // TODO investigate if it is worth to only do reverse order copy if there is actual overlap
368
369        // Specification step 14.
370        if destination_index <= source_index {
371            // if source index is bigger than or equal to destination index, forward processing copy
372            // handles overlaps just fine
373            (0..count).for_each(copy_one_byte)
374        }
375        // Specification step 15.
376        else {
377            // if source index is smaller than destination index, backward processing is required to
378            // avoid data loss on overlaps
379            (0..count).rev().for_each(copy_one_byte)
380        }
381
382        Ok(())
383    }
384
385    // Rationale behind having `source_index` and `count` when the callsite could also just create a
386    // subslice for `source_data`? Have all the index error checks in one place.
387    //
388    // <https://webassembly.github.io/spec/core/exec/instructions.html#xref-syntax-instructions-syntax-instr-memory-mathsf-memory-init-x>
389    pub fn init(
390        &self,
391        destination_index: usize,
392        source_data: &[u8],
393        source_index: usize,
394        count: usize,
395    ) -> Result<(), RuntimeError> {
396        // self is the destination
397        let lock_guard_self = self.inner_data.read();
398        let data_len = source_data.len();
399
400        /* check source for out of bounds access */
401        // Specification step 16.
402        if count > data_len {
403            error!("init count is bigger than the data instance");
404            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
405        }
406
407        // Specification step 16.
408        if source_index > data_len - count {
409            error!("init source extends beyond the data instance's end");
410            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
411        }
412
413        /* check destination for out of bounds access */
414        // Specification step 16.
415        if count > lock_guard_self.len() {
416            error!("init count is bigger than the linear memory");
417            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
418        }
419
420        // Specification step 16.
421        if destination_index > lock_guard_self.len() - count {
422            error!("init extends beyond the linear memory's end");
423            return Err(TrapError::MemoryOrDataAccessOutOfBounds.into());
424        }
425
426        /* check if there is anything to be done */
427        // Specification step 17.
428        if count == 0 {
429            return Ok(());
430        }
431
432        /* do the init */
433        // Specification step 18-27.
434        for i in 0..count {
435            // SAFETY:
436            // The safety of this `unsafe` block depends on the index being valid, which it is
437            // because:
438            //
439            // - the first if statement in this function guarantees that `count` elements can fit
440            //   into the `LinearMemory` `&source_mem`
441            // - the second if statement in this function guarantees that even with the offset
442            //   `source_index`, writing all `count`'s bytes does not extend beyond the last byte in
443            let src_byte = unsafe { source_data.get_unchecked(i + source_index) };
444
445            // SAFETY:
446            // The safety of this `unsafe` block depends on the index being valid, which it is
447            // because:
448            //
449            // - the third if statement in this function guarantees that `count` elements can fit
450            //   into the `LinearMemory` `&self`
451            // - the fourth if statement in this function guarantees that even with the offset
452            //   `destination_index`, writing all `count`'s bytes does not extend beyond the last byte in
453            //   the `LinearMemory` `&self`
454            let dst_byte = unsafe { lock_guard_self.get_unchecked(i + destination_index) };
455            dst_byte.store(*src_byte, Ordering::Relaxed);
456        }
457
458        Ok(())
459    }
460
461    /// Allows a given closure to temporarily access the entire memory as a
462    /// `&mut [u8]`.
463    ///
464    /// # Note on locking
465    ///
466    /// This operation exclusively locks the entire linear memory for the
467    /// duration of this function call. To acquire the lock, this function may
468    /// also block until the lock is available.
469    pub fn access_mut_slice<R>(&self, accessor: impl FnOnce(&mut [u8]) -> R) -> R {
470        /// Converts an exclusively borrowed slice of atomic `u8`s to a slice of
471        /// non-atomic `u8`s
472        // TODO when `atomic_from_mut` is stabilized, replace this function with
473        // `Atomic::U8::get_mut_slice`
474        fn atomic_u8_get_mut_slice(slice: &mut [AtomicU8]) -> &mut [u8] {
475            // SAFETY: the mutable reference guarantees unique ownership
476            unsafe { &mut *(slice as *mut [AtomicU8] as *mut [u8]) }
477        }
478
479        let mut write_lock_guard = self.inner_data.write();
480        let non_atomic_slice = atomic_u8_get_mut_slice(&mut write_lock_guard);
481        accessor(non_atomic_slice)
482    }
483}
484
485impl<const PAGE_SIZE: usize> core::fmt::Debug for LinearMemory<PAGE_SIZE> {
486    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
487        /// A helper struct for formatting a [`Vec<UnsafeCell<u8>>`] which is guarded by a [`ReadLockGuard`].
488        /// This formatter is able to detect and format byte repetitions in a compact way.
489        struct RepetitionDetectingMemoryWriter<'a>(ReadLockGuard<'a, Vec<AtomicU8>>);
490        impl core::fmt::Debug for RepetitionDetectingMemoryWriter<'_> {
491            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
492                /// The number of repetitions required for successive elements to be grouped
493                // together.
494                const MIN_REPETITIONS_FOR_GROUP: usize = 8;
495
496                // First we create an iterator over all bytes
497                let mut bytes = self.0.iter().map(|x| x.load(Ordering::Relaxed));
498
499                // Then we iterate over all bytes and deduplicate repetitions. This produces an
500                // iterator of pairs, consisting of the number of repetitions and the repeated byte
501                // itself. `current_group` is captured by the iterator and used as state to track
502                // the current group.
503                let mut current_group: Option<(usize, u8)> = None;
504                let deduplicated_with_count = iter::from_fn(|| {
505                    for byte in bytes.by_ref() {
506                        // If the next byte is different than the one being tracked currently...
507                        if current_group.is_some() && current_group.unwrap().1 != byte {
508                            // ...then end and emit the current group but also start a new group for
509                            // the next byte with an initial count of 1.
510                            return current_group.replace((1, byte));
511                        }
512                        // Otherwise increment the current group's counter or start a new group if
513                        // this was the first byte.
514                        current_group.get_or_insert((0, byte)).0 += 1;
515                    }
516                    // In the end when there are no more bytes to read, directly emit the last
517                    current_group.take()
518                });
519
520                // Finally we use `DebugList` to print a list of all groups, while writing out all
521                // elements from groups with less than `MIN_REPETITIONS_FOR_GROUP` elements.
522                let mut list = f.debug_list();
523                deduplicated_with_count.for_each(|(count, value)| {
524                    if count < MIN_REPETITIONS_FOR_GROUP {
525                        list.entries(iter::repeat_n(value, count));
526                    } else {
527                        list.entry(&format_args!("#{count} × {value}"));
528                    }
529                });
530                list.finish()
531            }
532        }
533
534        // Format the linear memory by using Rust's formatter helpers and the previously defined
535        // `RepetitionDetectingMemoryWriter`
536        f.debug_struct("LinearMemory")
537            .field(
538                "inner_data",
539                &RepetitionDetectingMemoryWriter(self.inner_data.read()),
540            )
541            .finish()
542    }
543}
544
545impl<const PAGE_SIZE: usize> Default for LinearMemory<PAGE_SIZE> {
546    fn default() -> Self {
547        Self::new()
548    }
549}
550
551#[cfg(test)]
552mod test {
553    use core::f64;
554
555    use alloc::format;
556    use core::mem;
557
558    use crate::{F32, F64};
559
560    use super::*;
561
562    const PAGE_SIZE: usize = 1 << 8;
563    const PAGES: PageCountTy = 2;
564
565    #[test]
566    fn new_constructor() {
567        let lin_mem = LinearMemory::<PAGE_SIZE>::new();
568        assert_eq!(lin_mem.pages(), 0);
569    }
570
571    #[test]
572    fn new_grow() {
573        let lin_mem = LinearMemory::<PAGE_SIZE>::new();
574        lin_mem.grow(1);
575        assert_eq!(lin_mem.pages(), 1);
576    }
577
578    #[test]
579    fn debug_print_simple() {
580        let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(1);
581        assert_eq!(lin_mem.pages(), 1);
582
583        let expected = format!("LinearMemory {{ inner_data: [#{PAGE_SIZE} × 0] }}");
584        let debug_repr = format!("{lin_mem:?}");
585
586        assert_eq!(debug_repr, expected);
587    }
588
589    #[test]
590    fn debug_print_complex() {
591        let page_count = 2;
592        let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(page_count);
593        assert_eq!(lin_mem.pages(), page_count);
594
595        lin_mem.store(1, 0xffu8).unwrap();
596        lin_mem.store(10, 1u8).unwrap();
597        lin_mem.store(200, 0xffu8).unwrap();
598
599        let expected = "LinearMemory { inner_data: [0, 255, #8 × 0, 1, #189 × 0, 255, #311 × 0] }";
600        let debug_repr = format!("{lin_mem:?}");
601
602        assert_eq!(debug_repr, expected);
603    }
604
605    #[test]
606    fn debug_print_empty() {
607        let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(0);
608        assert_eq!(lin_mem.pages(), 0);
609
610        let expected = "LinearMemory { inner_data: [] }";
611        let debug_repr = format!("{lin_mem:?}");
612
613        assert_eq!(debug_repr, expected);
614    }
615
616    #[test]
617    fn roundtrip_normal_range_i8_neg127() {
618        let x: i8 = -127;
619        let highest_legal_offset = PAGE_SIZE - mem::size_of::<i8>();
620        for offset in 0..highest_legal_offset {
621            let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(PAGES);
622
623            lin_mem.store(offset, x).unwrap();
624
625            assert_eq!(
626                lin_mem
627                    .load::<{ core::mem::size_of::<i8>() }, i8>(offset)
628                    .unwrap(),
629                x,
630                "load store roundtrip for {x:?} failed!"
631            );
632        }
633    }
634
635    #[test]
636    fn roundtrip_normal_range_f32_13() {
637        let x = F32(13.0);
638        let highest_legal_offset = PAGE_SIZE - mem::size_of::<F32>();
639        for offset in 0..highest_legal_offset {
640            let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(PAGES);
641
642            lin_mem.store(offset, x).unwrap();
643
644            assert_eq!(
645                lin_mem
646                    .load::<{ core::mem::size_of::<F32>() }, F32>(offset)
647                    .unwrap(),
648                x,
649                "load store roundtrip for {x:?} failed!"
650            );
651        }
652    }
653
654    #[test]
655    fn roundtrip_normal_range_f64_min() {
656        let x = F64(f64::MIN);
657        let highest_legal_offset = PAGE_SIZE - mem::size_of::<F64>();
658        for offset in 0..highest_legal_offset {
659            let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(PAGES);
660
661            lin_mem.store(offset, x).unwrap();
662
663            assert_eq!(
664                lin_mem
665                    .load::<{ core::mem::size_of::<F64>() }, F64>(offset)
666                    .unwrap(),
667                x,
668                "load store roundtrip for {x:?} failed!"
669            );
670        }
671    }
672
673    #[test]
674    fn roundtrip_normal_range_f64_nan() {
675        let x = F64(f64::NAN);
676        let highest_legal_offset = PAGE_SIZE - mem::size_of::<f64>();
677        for offset in 0..highest_legal_offset {
678            let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(PAGES);
679
680            lin_mem.store(offset, x).unwrap();
681
682            assert!(
683                lin_mem
684                    .load::<{ core::mem::size_of::<F64>() }, F64>(offset)
685                    .unwrap()
686                    .is_nan(),
687                "load store roundtrip for {x:?} failed!"
688            );
689        }
690    }
691
692    #[test]
693    #[should_panic(
694        expected = "called `Result::unwrap()` on an `Err` value: Trap(MemoryOrDataAccessOutOfBounds)"
695    )]
696    fn store_out_of_range_u128_max() {
697        let x: u128 = u128::MAX;
698        let pages = 1;
699        let lowest_illegal_offset = PAGE_SIZE - mem::size_of::<u128>() + 1;
700        let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(pages);
701
702        lin_mem.store(lowest_illegal_offset, x).unwrap();
703    }
704
705    #[test]
706    #[should_panic(
707        expected = "called `Result::unwrap()` on an `Err` value: Trap(MemoryOrDataAccessOutOfBounds)"
708    )]
709    fn store_empty_lineaer_memory_u8() {
710        let x: u8 = u8::MAX;
711        let pages = 0;
712        let lowest_illegal_offset = PAGE_SIZE - mem::size_of::<u8>() + 1;
713        let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(pages);
714
715        lin_mem.store(lowest_illegal_offset, x).unwrap();
716    }
717
718    #[test]
719    #[should_panic(
720        expected = "called `Result::unwrap()` on an `Err` value: Trap(MemoryOrDataAccessOutOfBounds)"
721    )]
722    fn load_out_of_range_u128_max() {
723        let pages = 1;
724        let lowest_illegal_offset = PAGE_SIZE - mem::size_of::<u128>() + 1;
725        let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(pages);
726
727        let _x: u128 = lin_mem.load(lowest_illegal_offset).unwrap();
728    }
729
730    #[test]
731    #[should_panic(
732        expected = "called `Result::unwrap()` on an `Err` value: Trap(MemoryOrDataAccessOutOfBounds)"
733    )]
734    fn load_empty_lineaer_memory_u8() {
735        let pages = 0;
736        let lowest_illegal_offset = PAGE_SIZE - mem::size_of::<u8>() + 1;
737        let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(pages);
738
739        let _x: u8 = lin_mem.load(lowest_illegal_offset).unwrap();
740    }
741
742    #[test]
743    #[should_panic]
744    fn copy_out_of_bounds() {
745        let lin_mem_0 = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(2);
746        let lin_mem_1 = LinearMemory::<PAGE_SIZE>::new_with_initial_pages(1);
747        lin_mem_0.copy(0, &lin_mem_1, 0, PAGE_SIZE + 1).unwrap();
748    }
749}