Skip to content

Commit b432874

Browse files
author
Arnaud Riess
committed
test: introduced heavy fibo
1 parent 65f2217 commit b432874

9 files changed

Lines changed: 342 additions & 6 deletions

File tree

crates/herkos-runtime/src/memory.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,27 @@ impl<const MAX_PAGES: usize> IsolatedMemory<MAX_PAGES> {
9393
self.pages.as_flattened_mut()
9494
}
9595

96+
// ── Bulk memory operations ────────────────────────────────────────
97+
98+
/// Wasm `memory.copy` — copy `len` bytes from `src` to `dst`.
99+
///
100+
/// Semantics match `memmove`: overlapping source and destination regions
101+
/// are handled correctly. Traps (`OutOfBounds`) if either region extends
102+
/// beyond the current active memory.
103+
pub fn memory_copy(&mut self, dst: u32, src: u32, len: u32) -> WasmResult<()> {
104+
let active = self.active_size();
105+
let dst = dst as usize;
106+
let src = src as usize;
107+
let len = len as usize;
108+
if src.checked_add(len).is_none_or(|end| end > active)
109+
|| dst.checked_add(len).is_none_or(|end| end > active)
110+
{
111+
return Err(WasmTrap::OutOfBounds);
112+
}
113+
self.flat_mut().copy_within(src..src + len, dst);
114+
Ok(())
115+
}
116+
96117
// ── Bounds-checked (safe) load/store ──────────────────────────────
97118

98119
/// Load an i32 from linear memory with bounds checking.

crates/herkos-runtime/src/ops.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ use crate::{WasmResult, WasmTrap};
2727
/// Wasm `i32.trunc_f32_s`: truncate f32 toward zero to i32, trapping on NaN/overflow.
2828
#[inline(never)]
2929
pub fn i32_trunc_f32_s(v: f32) -> WasmResult<i32> {
30-
if v.is_nan() || v >= 2147483648.0f32 || v < -2147483648.0f32 {
30+
if v.is_nan() || !(-2147483648.0f32..2147483648.0f32).contains(&v) {
3131
return Err(WasmTrap::IntegerOverflow);
3232
}
3333
Ok(v as i32)
@@ -49,7 +49,7 @@ pub fn i32_trunc_f32_u(v: f32) -> WasmResult<i32> {
4949
/// Wasm `i32.trunc_f64_s`: truncate f64 toward zero to i32, trapping on NaN/overflow.
5050
#[inline(never)]
5151
pub fn i32_trunc_f64_s(v: f64) -> WasmResult<i32> {
52-
if v.is_nan() || v >= 2147483648.0f64 || v < -2147483648.0f64 {
52+
if v.is_nan() || !(-2147483648.0f64..2147483648.0f64).contains(&v) {
5353
return Err(WasmTrap::IntegerOverflow);
5454
}
5555
Ok(v as i32)
@@ -70,7 +70,7 @@ pub fn i32_trunc_f64_u(v: f64) -> WasmResult<i32> {
7070
/// Wasm `i64.trunc_f32_s`: truncate f32 toward zero to i64, trapping on NaN/overflow.
7171
#[inline(never)]
7272
pub fn i64_trunc_f32_s(v: f32) -> WasmResult<i64> {
73-
if v.is_nan() || v >= 9223372036854775808.0f32 || v < -9223372036854775808.0f32 {
73+
if v.is_nan() || !(-9223372036854775808.0f32..9223372036854775808.0f32).contains(&v) {
7474
return Err(WasmTrap::IntegerOverflow);
7575
}
7676
Ok(v as i64)
@@ -89,7 +89,7 @@ pub fn i64_trunc_f32_u(v: f32) -> WasmResult<i64> {
8989
/// Wasm `i64.trunc_f64_s`: truncate f64 toward zero to i64, trapping on NaN/overflow.
9090
#[inline(never)]
9191
pub fn i64_trunc_f64_s(v: f64) -> WasmResult<i64> {
92-
if v.is_nan() || v >= 9223372036854775808.0f64 || v < -9223372036854775808.0f64 {
92+
if v.is_nan() || !(-9223372036854775808.0f64..9223372036854775808.0f64).contains(&v) {
9393
return Err(WasmTrap::IntegerOverflow);
9494
}
9595
Ok(v as i64)
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#![no_std]
2+
extern crate alloc;
3+
4+
use alloc::vec::Vec;
5+
use core::alloc::{GlobalAlloc, Layout};
6+
7+
// ── Panic handler ─────────────────────────────────────────────────────────────
8+
9+
#[panic_handler]
10+
fn panic(_info: &core::panic::PanicInfo) -> ! {
11+
loop {}
12+
}
13+
14+
// ── Bump allocator ────────────────────────────────────────────────────────────
15+
//
16+
// A simple single-threaded bump allocator backed by a static byte array.
17+
// Memory is carved out by advancing a cursor; individual frees are no-ops.
18+
// This is appropriate for a `no_std` Wasm module that runs in a single thread
19+
// and has bounded, predictable allocation needs.
20+
21+
const HEAP_SIZE: usize = 16 * 1024; // 16 KiB — ample for thousands of cached i32 values
22+
23+
static mut HEAP: [u8; HEAP_SIZE] = [0u8; HEAP_SIZE];
24+
static mut HEAP_CURSOR: usize = 0;
25+
26+
struct BumpAllocator;
27+
28+
unsafe impl GlobalAlloc for BumpAllocator {
29+
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
30+
let align = layout.align();
31+
let size = layout.size();
32+
// Round cursor up to the required alignment.
33+
let aligned = (HEAP_CURSOR + align - 1) & !(align - 1);
34+
let new_cursor = aligned + size;
35+
if new_cursor > HEAP_SIZE {
36+
return core::ptr::null_mut(); // out of heap space
37+
}
38+
HEAP_CURSOR = new_cursor;
39+
HEAP.as_mut_ptr().add(aligned)
40+
}
41+
42+
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
43+
// Bump allocators never free individual allocations.
44+
}
45+
}
46+
47+
#[global_allocator]
48+
static ALLOCATOR: BumpAllocator = BumpAllocator;
49+
50+
// ── Fibonacci cache ───────────────────────────────────────────────────────────
51+
//
52+
// `CACHE` is a lazily-initialised `Vec` that stores:
53+
// CACHE[0] = fibo(0), CACHE[1] = fibo(1), …, CACHE[k-1] = fibo(k-1)
54+
//
55+
// On each call to `fibo(n)`:
56+
// • If k > n — the value is already cached; return it directly (O(1)).
57+
// • If k <= n — extend the cache from index k up to n, then return (O(m)).
58+
//
59+
// All arithmetic uses wrapping i32 semantics to match WebAssembly behaviour.
60+
61+
static mut CACHE: Option<Vec<i32>> = None;
62+
63+
/// Return a mutable reference to the cache, initialising it on first access.
64+
fn cache() -> &'static mut Vec<i32> {
65+
// SAFETY: WebAssembly modules are single-threaded; there is no concurrent
66+
// access to `CACHE`. We only call this function from exported entry points
67+
// that Wasm guarantees are not re-entered.
68+
unsafe {
69+
if CACHE.is_none() {
70+
let mut v = Vec::with_capacity(64);
71+
v.push(0i32); // fibo(0)
72+
v.push(1i32); // fibo(1)
73+
CACHE = Some(v);
74+
}
75+
// SAFETY: we just guaranteed `CACHE` is `Some`.
76+
CACHE.as_mut().unwrap_unchecked()
77+
}
78+
}
79+
80+
/// Return `fibo(n)` using wrapping i32 arithmetic (matches Wasm semantics).
81+
///
82+
/// All previously unseen values from `fibo(cache_len)` up to `fibo(n)` are
83+
/// computed once and appended to the cache. Subsequent calls with the same
84+
/// or a smaller `n` are served directly from the cache without recomputation.
85+
#[no_mangle]
86+
pub extern "C" fn fibo(n: i32) -> i32 {
87+
if n < 0 {
88+
return 0;
89+
}
90+
let n = n as usize;
91+
let v = cache();
92+
while v.len() <= n {
93+
let k = v.len();
94+
let next = v[k - 2].wrapping_add(v[k - 1]);
95+
v.push(next);
96+
}
97+
v[n]
98+
}
99+
100+
/// Return the number of Fibonacci values currently stored in the cache.
101+
///
102+
/// Calling this function before any `fibo` call will return 2 because the
103+
/// cache is pre-seeded with `fibo(0) = 0` and `fibo(1) = 1`.
104+
#[no_mangle]
105+
pub extern "C" fn fibo_cache_len() -> i32 {
106+
cache().len() as i32
107+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
//! End-to-end tests: Rust → Wasm → Rust (memoised Fibonacci with custom allocator).
2+
//!
3+
//! The source module (`data/rust/rust_e2e_heavy_fibo.rs`) uses a bump allocator
4+
//! to provide `liballoc` in a `no_std` Wasm environment. It caches computed
5+
//! Fibonacci values in a `Vec<i32>` so that:
6+
//!
7+
//! * Calling `fibo(n)` when `n` is already cached costs O(1).
8+
//! * Calling `fibo(n + m)` after a previous `fibo(n)` only computes the
9+
//! `m` new values — all earlier results are served from the cache.
10+
//!
11+
//! `fibo_cache_len()` exposes the current cache size so tests can verify the
12+
//! memoisation invariants without inspecting internal state directly.
13+
14+
use herkos_tests::rust_e2e_heavy_fibo;
15+
16+
fn new_module() -> rust_e2e_heavy_fibo::WasmModule {
17+
rust_e2e_heavy_fibo::new().expect("module instantiation should succeed")
18+
}
19+
20+
// ── Reference implementation ──────────────────────────────────────────────────
21+
22+
/// Ground-truth `fibo(n)` using the same wrapping i32 arithmetic as Wasm.
23+
fn fibo_ref(n: usize) -> i32 {
24+
let mut a: i32 = 0;
25+
let mut b: i32 = 1;
26+
for _ in 0..n {
27+
let tmp = a.wrapping_add(b);
28+
a = b;
29+
b = tmp;
30+
}
31+
a
32+
}
33+
34+
// ── Base cases ────────────────────────────────────────────────────────────────
35+
36+
#[test]
37+
fn test_fibo_base_cases() {
38+
let mut m = new_module();
39+
assert_eq!(m.fibo(0).unwrap(), 0);
40+
assert_eq!(m.fibo(1).unwrap(), 1);
41+
}
42+
43+
// ── Small known values ────────────────────────────────────────────────────────
44+
45+
#[test]
46+
fn test_fibo_small_values() {
47+
let mut m = new_module();
48+
// Classical Fibonacci sequence: 0,1,1,2,3,5,8,13,21,34,55,89,144
49+
let expected: &[i32] = &[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144];
50+
for (n, &want) in expected.iter().enumerate() {
51+
assert_eq!(m.fibo(n as i32).unwrap(), want, "fibo({n})");
52+
}
53+
}
54+
55+
// ── Cross-validation against reference ───────────────────────────────────────
56+
57+
#[test]
58+
fn test_fibo_matches_reference() {
59+
let mut m = new_module();
60+
for n in 0i32..50 {
61+
assert_eq!(m.fibo(n).unwrap(), fibo_ref(n as usize), "fibo({n})");
62+
}
63+
}
64+
65+
// ── Negative input guard ──────────────────────────────────────────────────────
66+
67+
#[test]
68+
fn test_fibo_negative_returns_zero() {
69+
let mut m = new_module();
70+
assert_eq!(m.fibo(-1).unwrap(), 0);
71+
assert_eq!(m.fibo(-100).unwrap(), 0);
72+
}
73+
74+
// ── Cache growth ──────────────────────────────────────────────────────────────
75+
76+
#[test]
77+
fn test_cache_seeded_with_two_entries() {
78+
let mut m = new_module();
79+
// The cache is pre-seeded with fibo(0) = 0 and fibo(1) = 1 on first access.
80+
assert_eq!(m.fibo_cache_len().unwrap(), 2);
81+
}
82+
83+
#[test]
84+
fn test_cache_grows_to_cover_requested_index() {
85+
let mut m = new_module();
86+
m.fibo(10).unwrap();
87+
// Cache must cover indices 0..=10, i.e. 11 entries.
88+
assert_eq!(m.fibo_cache_len().unwrap(), 11);
89+
}
90+
91+
#[test]
92+
fn test_cache_extends_incrementally() {
93+
let mut m = new_module();
94+
95+
m.fibo(10).unwrap();
96+
assert_eq!(m.fibo_cache_len().unwrap(), 11);
97+
98+
// Requesting fibo(20) only needs to compute 10 new values.
99+
m.fibo(20).unwrap();
100+
assert_eq!(m.fibo_cache_len().unwrap(), 21);
101+
102+
m.fibo(30).unwrap();
103+
assert_eq!(m.fibo_cache_len().unwrap(), 31);
104+
}
105+
106+
#[test]
107+
fn test_cache_does_not_shrink_on_smaller_query() {
108+
let mut m = new_module();
109+
m.fibo(20).unwrap();
110+
let len_after = m.fibo_cache_len().unwrap();
111+
112+
// A smaller query must not evict cached entries.
113+
m.fibo(5).unwrap();
114+
assert_eq!(
115+
m.fibo_cache_len().unwrap(),
116+
len_after,
117+
"cache must not shrink after a smaller query"
118+
);
119+
}
120+
121+
// ── Monotone cache invariant ──────────────────────────────────────────────────
122+
123+
#[test]
124+
fn test_cache_len_is_monotone() {
125+
let mut m = new_module();
126+
let mut prev_len = m.fibo_cache_len().unwrap();
127+
128+
// Mix of increasing and decreasing n values; cache must never shrink.
129+
for n in [0i32, 3, 1, 10, 7, 20, 20, 25, 24, 30] {
130+
m.fibo(n).unwrap();
131+
let new_len = m.fibo_cache_len().unwrap();
132+
assert!(
133+
new_len >= prev_len,
134+
"cache len should be monotone non-decreasing (was {prev_len}, now {new_len} after fibo({n}))"
135+
);
136+
prev_len = new_len;
137+
}
138+
}
139+
140+
// ── Wrapping arithmetic beyond i32 overflow ───────────────────────────────────
141+
142+
#[test]
143+
fn test_fibo_wrapping_overflow() {
144+
let mut m = new_module();
145+
// fibo(46) = 1_836_311_903 is the last value that fits in i32.
146+
// fibo(47) and beyond overflow — wrapping behaviour must match the reference.
147+
for n in 44i32..=55 {
148+
assert_eq!(
149+
m.fibo(n).unwrap(),
150+
fibo_ref(n as usize),
151+
"fibo({n}) wrapping"
152+
);
153+
}
154+
}
155+
156+
// ── Idempotency: repeated calls return identical results ──────────────────────
157+
158+
#[test]
159+
fn test_fibo_idempotent() {
160+
let mut m = new_module();
161+
// Prime the cache up to n=30.
162+
for n in 0i32..=30 {
163+
m.fibo(n).unwrap();
164+
}
165+
// All values must be stable on repeated queries.
166+
for n in 0i32..=30 {
167+
assert_eq!(
168+
m.fibo(n).unwrap(),
169+
fibo_ref(n as usize),
170+
"fibo({n}) should be idempotent"
171+
);
172+
}
173+
}

crates/herkos/src/backend/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ pub trait Backend {
8989
/// Emit Rust code for memory.grow (grows by delta pages, returns old size or -1).
9090
fn emit_memory_grow(&self, dest: VarId, delta: VarId) -> String;
9191

92+
/// Emit Rust code for memory.copy (copies len bytes from src to dst).
93+
fn emit_memory_copy(&self, dst: VarId, src: VarId, len: VarId) -> String;
94+
9295
/// Emit Rust code for unreachable.
9396
fn emit_unreachable(&self) -> String;
9497

crates/herkos/src/backend/safe.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,7 @@ impl Backend for SafeBackend {
347347
sign: Option<SignExtension>,
348348
) -> anyhow::Result<String> {
349349
let addr_expr = if offset > 0 {
350-
format!("({addr} as usize).wrapping_add({offset} as usize)")
350+
format!("({addr} as usize).wrapping_add({offset}_usize)")
351351
} else {
352352
format!("{addr} as usize")
353353
};
@@ -423,7 +423,7 @@ impl Backend for SafeBackend {
423423
width: MemoryAccessWidth,
424424
) -> anyhow::Result<String> {
425425
let addr_expr = if offset > 0 {
426-
format!("({addr} as usize).wrapping_add({offset} as usize)")
426+
format!("({addr} as usize).wrapping_add({offset}_usize)")
427427
} else {
428428
format!("{addr} as usize")
429429
};
@@ -537,6 +537,10 @@ impl Backend for SafeBackend {
537537
format!(" {dest} = memory.grow({delta} as u32);")
538538
}
539539

540+
fn emit_memory_copy(&self, dst: VarId, src: VarId, len: VarId) -> String {
541+
format!(" memory.memory_copy({dst} as u32, {src} as u32, {len} as u32)?;")
542+
}
543+
540544
fn emit_unreachable(&self) -> String {
541545
" return Err(WasmTrap::Unreachable);".to_string()
542546
}

crates/herkos/src/codegen/instruction.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ pub fn generate_instruction_with_info<B: Backend>(
9595

9696
IrInstr::MemoryGrow { dest, delta } => backend.emit_memory_grow(*dest, *delta),
9797

98+
IrInstr::MemoryCopy { dst, src, len } => backend.emit_memory_copy(*dst, *src, *len),
99+
98100
IrInstr::Select {
99101
dest,
100102
val1,

0 commit comments

Comments
 (0)