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}