Skip to main content

herkos_runtime/
table.rs

1//! Wasm indirect call table — supports `call_indirect`.
2//!
3//! A Wasm table is a vector of nullable function references. `call_indirect`
4//! looks up an entry by index, checks its type signature, and dispatches.
5//!
6//! In transpiled Rust, function pointers have heterogeneous types (different
7//! params/results). We store them as type-erased `FuncRef` entries: each
8//! entry carries a `type_index` (the Wasm type section index) and an opaque
9//! function pointer. The transpiler generates a match-based dispatch that
10//! casts the pointer back to the correct concrete type after verifying the
11//! type index.
12//!
13//! The table uses a fixed-size backing array (const generic `MAX_SIZE`)
14//! to stay `no_std` compatible.
15
16use crate::{WasmResult, WasmTrap};
17
18/// A single table entry: a typed function reference.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct FuncRef {
21    /// Index into the module's type section. Used by `call_indirect` to
22    /// verify the caller's expected signature matches the callee's actual
23    /// signature. A mismatch is a trap (`IndirectCallTypeMismatch`).
24    pub type_index: u32,
25    /// Index into the module's function index space. The transpiler
26    /// generates a match/dispatch over this value to call the right
27    /// concrete Rust function.
28    pub func_index: u32,
29}
30
31/// Indirect call table with a compile-time maximum size.
32///
33/// `MAX_SIZE` is derived from the Wasm module's table declaration.
34/// Entries are `Option<FuncRef>` — `None` means the slot is empty
35/// (calling it traps with `UndefinedElement`).
36pub struct Table<const MAX_SIZE: usize> {
37    entries: [Option<FuncRef>; MAX_SIZE],
38    /// Current number of initialized entries (analogous to `active_pages`).
39    /// Accesses at or beyond this index trap with `TableOutOfBounds`.
40    active_size: usize,
41}
42
43impl<const MAX_SIZE: usize> Table<MAX_SIZE> {
44    /// Create a new table with `initial_size` slots, all empty (`None`).
45    ///
46    /// # Errors
47    /// Returns `ConstructionError::TableInitialSizeExceedsMax` if `initial_size > MAX_SIZE`.
48    pub fn try_new(initial_size: usize) -> Result<Self, crate::ConstructionError> {
49        if initial_size > MAX_SIZE {
50            return Err(crate::ConstructionError::TableInitialSizeExceedsMax {
51                initial: initial_size,
52                max: MAX_SIZE,
53            });
54        }
55        Ok(Self {
56            entries: [None; MAX_SIZE],
57            active_size: initial_size,
58        })
59    }
60
61    /// Current number of active table slots.
62    #[inline(always)]
63    pub fn size(&self) -> usize {
64        self.active_size
65    }
66
67    /// Look up a table entry by index. Returns the `FuncRef` if present.
68    ///
69    /// - `TableOutOfBounds` if `index >= active_size`
70    /// - `UndefinedElement` if the slot is `None`
71    #[inline]
72    pub fn get(&self, index: u32) -> WasmResult<FuncRef> {
73        let idx = index as usize;
74        if idx >= self.active_size {
75            return Err(WasmTrap::TableOutOfBounds);
76        }
77        self.entries
78            .get(idx)
79            .copied()
80            .flatten()
81            .ok_or(WasmTrap::UndefinedElement)
82    }
83
84    /// Set a table entry. Used during module initialization (element segments).
85    ///
86    /// Returns `Err(TableOutOfBounds)` if `index >= active_size`.
87    #[inline]
88    pub fn set(&mut self, index: u32, entry: Option<FuncRef>) -> WasmResult<()> {
89        let idx = index as usize;
90        if idx >= self.active_size {
91            return Err(WasmTrap::TableOutOfBounds);
92        }
93        match self.entries.get_mut(idx) {
94            Some(slot) => {
95                *slot = entry;
96                Ok(())
97            }
98            None => Err(WasmTrap::TableOutOfBounds),
99        }
100    }
101
102    /// Initialize table entries from element segment data.
103    ///
104    /// Writes `entries` (each as `(type_index, func_index)`) into consecutive
105    /// slots starting at `base`. Replaces per-slot `set()` calls in generated
106    /// constructors and propagates bounds errors via `?` instead of panicking.
107    ///
108    /// # Errors
109    /// Returns `Err(TableOutOfBounds)` if any slot index is out of range.
110    #[inline(always)]
111    pub fn init_elements(&mut self, base: u32, entries: &[(u32, u32)]) -> WasmResult<()> {
112        init_elements_inner(&mut self.entries, self.active_size, base, entries)
113    }
114
115    /// Grow the table by `delta` slots, filling new slots with `init`.
116    /// Returns the previous size, or -1 on failure.
117    pub fn grow(&mut self, delta: u32, init: Option<FuncRef>) -> i32 {
118        let old = self.active_size;
119        let new = old.wrapping_add(delta as usize);
120        if new > MAX_SIZE {
121            return -1;
122        }
123        for slot in &mut self.entries[old..new] {
124            *slot = init;
125        }
126        self.active_size = new;
127        old as i32
128    }
129}
130
131// ── Non-generic inner function (outline pattern, §13.3) ──────────────────────
132
133#[inline(never)]
134fn init_elements_inner(
135    slots: &mut [Option<FuncRef>],
136    active_size: usize,
137    base: u32,
138    entries: &[(u32, u32)],
139) -> WasmResult<()> {
140    for (i, &(type_index, func_index)) in entries.iter().enumerate() {
141        let idx = (base as usize)
142            .checked_add(i)
143            .ok_or(WasmTrap::TableOutOfBounds)?;
144        if idx >= active_size {
145            return Err(WasmTrap::TableOutOfBounds);
146        }
147        match slots.get_mut(idx) {
148            Some(slot) => {
149                *slot = Some(FuncRef {
150                    type_index,
151                    func_index,
152                })
153            }
154            None => return Err(WasmTrap::TableOutOfBounds),
155        }
156    }
157    Ok(())
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    fn sample_ref(type_idx: u32, func_idx: u32) -> FuncRef {
165        FuncRef {
166            type_index: type_idx,
167            func_index: func_idx,
168        }
169    }
170
171    #[test]
172    fn new_table_is_empty() {
173        let table = Table::<8>::try_new(4).unwrap();
174        assert_eq!(table.size(), 4);
175        // All slots are None → UndefinedElement
176        assert_eq!(table.get(0), Err(WasmTrap::UndefinedElement));
177        assert_eq!(table.get(3), Err(WasmTrap::UndefinedElement));
178    }
179
180    #[test]
181    fn get_out_of_bounds() {
182        let table = Table::<8>::try_new(4).unwrap();
183        assert_eq!(table.get(4), Err(WasmTrap::TableOutOfBounds));
184        assert_eq!(table.get(100), Err(WasmTrap::TableOutOfBounds));
185    }
186
187    #[test]
188    fn set_and_get() {
189        let mut table = Table::<8>::try_new(4).unwrap();
190        let fr = sample_ref(0, 5);
191        table.set(2, Some(fr)).unwrap();
192
193        let got = table.get(2).unwrap();
194        assert_eq!(got.type_index, 0);
195        assert_eq!(got.func_index, 5);
196    }
197
198    #[test]
199    fn set_out_of_bounds() {
200        let mut table = Table::<8>::try_new(4).unwrap();
201        assert_eq!(
202            table.set(4, Some(sample_ref(0, 0))),
203            Err(WasmTrap::TableOutOfBounds)
204        );
205    }
206
207    #[test]
208    fn set_none_clears_entry() {
209        let mut table = Table::<8>::try_new(4).unwrap();
210        table.set(1, Some(sample_ref(0, 3))).unwrap();
211        assert!(table.get(1).is_ok());
212        table.set(1, None).unwrap();
213        assert_eq!(table.get(1), Err(WasmTrap::UndefinedElement));
214    }
215
216    #[test]
217    fn grow_success() {
218        let mut table = Table::<8>::try_new(2).unwrap();
219        let old = table.grow(3, None);
220        assert_eq!(old, 2);
221        assert_eq!(table.size(), 5);
222        // New slots are None
223        assert_eq!(table.get(4), Err(WasmTrap::UndefinedElement));
224    }
225
226    #[test]
227    fn grow_with_init() {
228        let mut table = Table::<8>::try_new(2).unwrap();
229        let fr = sample_ref(1, 7);
230        table.grow(2, Some(fr));
231        let got = table.get(3).unwrap();
232        assert_eq!(got.func_index, 7);
233    }
234
235    #[test]
236    fn grow_beyond_max_fails() {
237        let mut table = Table::<4>::try_new(2).unwrap();
238        assert_eq!(table.grow(3, None), -1); // would be 5 > 4
239        assert_eq!(table.size(), 2); // unchanged
240    }
241
242    // ── init_elements ──
243
244    #[test]
245    fn init_elements_writes_entries() {
246        let mut table = Table::<8>::try_new(4).unwrap();
247        table.init_elements(0, &[(1, 2), (3, 4)]).unwrap();
248        let e0 = table.get(0).unwrap();
249        assert_eq!(e0.type_index, 1);
250        assert_eq!(e0.func_index, 2);
251        let e1 = table.get(1).unwrap();
252        assert_eq!(e1.type_index, 3);
253        assert_eq!(e1.func_index, 4);
254    }
255
256    #[test]
257    fn init_elements_empty_is_noop() {
258        let mut table = Table::<4>::try_new(4).unwrap();
259        assert!(table.init_elements(0, &[]).is_ok());
260        assert_eq!(table.get(0), Err(WasmTrap::UndefinedElement));
261    }
262
263    #[test]
264    fn init_elements_at_base_offset() {
265        let mut table = Table::<8>::try_new(6).unwrap();
266        table.init_elements(3, &[(0, 5)]).unwrap();
267        assert_eq!(table.get(3).unwrap().func_index, 5);
268        assert_eq!(table.get(0), Err(WasmTrap::UndefinedElement));
269    }
270
271    #[test]
272    fn init_elements_out_of_bounds() {
273        let mut table = Table::<4>::try_new(4).unwrap();
274        // base=3, 2 entries → slots 3 and 4; slot 4 is OOB
275        let result = table.init_elements(3, &[(0, 0), (0, 1)]);
276        assert_eq!(result, Err(WasmTrap::TableOutOfBounds));
277    }
278
279    #[test]
280    fn init_elements_exactly_fills_table() {
281        let mut table = Table::<4>::try_new(4).unwrap();
282        table
283            .init_elements(0, &[(0, 0), (0, 1), (0, 2), (0, 3)])
284            .unwrap();
285        assert_eq!(table.get(3).unwrap().func_index, 3);
286    }
287
288    #[test]
289    fn try_new_fails_if_initial_exceeds_max() {
290        let result = Table::<4>::try_new(5);
291        assert!(result.is_err());
292        assert!(matches!(
293            result,
294            Err(crate::ConstructionError::TableInitialSizeExceedsMax { initial: 5, max: 4 })
295        ));
296    }
297}
298
299// ── Kani Formal Verification Proofs ──────────────────────────────────────
300
301#[cfg(kani)]
302mod proofs {
303    use super::*;
304
305    /// Proof: get never panics, only returns Ok or Err.
306    #[kani::proof]
307    #[kani::unwind(1)]
308    fn get_never_panics() {
309        let table = Table::<8>::new(4);
310        let index: u32 = kani::any();
311        let _ = table.get(index);
312        // Kani verifies this doesn't panic for all possible indices
313    }
314
315    /// Proof: set never panics, only returns Ok or Err.
316    #[kani::proof]
317    #[kani::unwind(1)]
318    fn set_never_panics() {
319        let mut table = Table::<8>::new(4);
320        let index: u32 = kani::any();
321        let type_index: u32 = kani::any();
322        let func_index: u32 = kani::any();
323        let entry = Some(FuncRef {
324            type_index,
325            func_index,
326        });
327        let _ = table.set(index, entry);
328    }
329
330    /// Proof: grow respects MAX_SIZE — active_size never exceeds it.
331    #[kani::proof]
332    #[kani::unwind(5)]
333    fn grow_respects_max_size() {
334        let mut table = Table::<4>::new(1);
335        let delta: u32 = kani::any();
336
337        let old_size = table.size();
338        let result = table.grow(delta, None);
339
340        // active_size must never exceed MAX_SIZE
341        kani::assert(table.size() <= 4, "active_size must not exceed MAX_SIZE");
342
343        // If grow succeeded, result should be old size
344        if result >= 0 {
345            kani::assert(result == old_size as i32, "grow returns old size");
346            let new_expected = old_size as u64 + delta as u64;
347            if new_expected <= 4 {
348                kani::assert(
349                    table.size() == new_expected as usize,
350                    "grow updates active_size correctly",
351                );
352            }
353        } else {
354            // If grow failed, active_size unchanged
355            kani::assert(
356                table.size() == old_size,
357                "failed grow leaves active_size unchanged",
358            );
359        }
360    }
361
362    /// Proof: grow returns -1 if new size would exceed MAX_SIZE.
363    #[kani::proof]
364    #[kani::unwind(4)]
365    fn grow_fails_beyond_max() {
366        let mut table = Table::<4>::new(2);
367        // Try to grow by 3 slots: 2 + 3 = 5 > 4 (MAX_SIZE)
368        let result = table.grow(3, None);
369        kani::assert(result == -1, "grow beyond MAX_SIZE returns -1");
370        kani::assert(table.size() == 2, "failed grow leaves size unchanged");
371    }
372
373    /// Proof: set followed by get returns the same value.
374    #[kani::proof]
375    #[kani::unwind(1)]
376    fn set_get_roundtrip() {
377        let mut table = Table::<8>::new(4);
378        let index: u32 = kani::any();
379        let type_index: u32 = kani::any();
380        let func_index: u32 = kani::any();
381
382        let entry = FuncRef {
383            type_index,
384            func_index,
385        };
386
387        // If set succeeds, get should return the same entry
388        if table.set(index, Some(entry)).is_ok() {
389            let result = table.get(index);
390            kani::assert(result.is_ok(), "get succeeds after successful set");
391            let retrieved = result.unwrap();
392            kani::assert(retrieved.type_index == type_index, "type_index preserved");
393            kani::assert(retrieved.func_index == func_index, "func_index preserved");
394        }
395    }
396
397    /// Proof: get out of bounds (index >= active_size) returns TableOutOfBounds.
398    #[kani::proof]
399    #[kani::unwind(1)]
400    fn get_out_of_bounds_returns_error() {
401        let table = Table::<8>::new(4);
402        // Access at or beyond active_size
403        let result1 = table.get(4);
404        kani::assert(
405            result1 == Err(WasmTrap::TableOutOfBounds),
406            "get at active_size is out of bounds",
407        );
408
409        let result2 = table.get(100);
410        kani::assert(
411            result2 == Err(WasmTrap::TableOutOfBounds),
412            "get beyond active_size is out of bounds",
413        );
414    }
415
416    /// Proof: set out of bounds (index >= active_size) returns TableOutOfBounds.
417    #[kani::proof]
418    #[kani::unwind(1)]
419    fn set_out_of_bounds_returns_error() {
420        let mut table = Table::<8>::new(4);
421        let entry = FuncRef {
422            type_index: 0,
423            func_index: 0,
424        };
425
426        let result = table.set(4, Some(entry));
427        kani::assert(
428            result == Err(WasmTrap::TableOutOfBounds),
429            "set at active_size is out of bounds",
430        );
431    }
432
433    /// Proof: get on empty slot returns UndefinedElement.
434    #[kani::proof]
435    #[kani::unwind(1)]
436    fn get_empty_slot_returns_undefined() {
437        let table = Table::<8>::new(4);
438        // All slots start empty
439        let result = table.get(0);
440        kani::assert(
441            result == Err(WasmTrap::UndefinedElement),
442            "get on empty slot returns UndefinedElement",
443        );
444    }
445
446    /// Proof: set None clears a slot.
447    #[kani::proof]
448    #[kani::unwind(1)]
449    fn set_none_clears_slot() {
450        let mut table = Table::<8>::new(4);
451        let entry = FuncRef {
452            type_index: 1,
453            func_index: 5,
454        };
455
456        // Set to Some, then clear with None
457        table.set(1, Some(entry)).unwrap();
458        kani::assert(table.get(1).is_ok(), "slot is set");
459
460        table.set(1, None).unwrap();
461        let result = table.get(1);
462        kani::assert(
463            result == Err(WasmTrap::UndefinedElement),
464            "set None clears the slot",
465        );
466    }
467
468    /// Proof: grow initializes new slots with init value.
469    #[kani::proof]
470    #[kani::unwind(3)]
471    fn grow_initializes_new_slots() {
472        let mut table = Table::<8>::new(2);
473        let init = FuncRef {
474            type_index: 7,
475            func_index: 42,
476        };
477
478        let result = table.grow(2, Some(init));
479
480        if result >= 0 {
481            // New slots should be initialized with init value
482            let slot2 = table.get(2);
483
484            if slot2.is_ok() {
485                let val = slot2.unwrap();
486                kani::assert(
487                    val.type_index == 7 && val.func_index == 42,
488                    "new slot initialized with init value",
489                );
490            }
491        }
492    }
493
494    /// Proof: size() returns active_size.
495    #[kani::proof]
496    #[kani::unwind(1)]
497    fn size_returns_active_size() {
498        let table = Table::<8>::new(5);
499        kani::assert(table.size() == 5, "size() returns active_size");
500    }
501
502    /// Proof: successful get requires index < active_size.
503    #[kani::proof]
504    #[kani::unwind(1)]
505    fn get_success_implies_valid_index() {
506        let mut table = Table::<8>::new(4);
507        // Set a valid entry
508        let entry = FuncRef {
509            type_index: 0,
510            func_index: 1,
511        };
512        table.set(0, Some(entry)).unwrap();
513
514        let index: u32 = kani::any();
515        let result = table.get(index);
516
517        // If get succeeds with a valid entry, index must be < active_size
518        if result.is_ok() {
519            kani::assert(
520                (index as usize) < table.size(),
521                "successful get implies index < active_size",
522            );
523        }
524    }
525
526    /// Proof: successful set requires index < active_size.
527    #[kani::proof]
528    #[kani::unwind(1)]
529    fn set_success_implies_valid_index() {
530        let mut table = Table::<8>::new(4);
531        let index: u32 = kani::any();
532        let entry = FuncRef {
533            type_index: 0,
534            func_index: 0,
535        };
536
537        let result = table.set(index, Some(entry));
538
539        if result.is_ok() {
540            kani::assert(
541                (index as usize) < table.size(),
542                "successful set implies index < active_size",
543            );
544        }
545    }
546}