Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions cranelift/docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ This indicates the following:
* `static`: We have requested a non-resizable and non-movable static heap.
* `size=0x1000`: It has to have a size of 4096 bytes.
* `ptr=vmctx+0`: The pointer to the address to the start of this heap is placed at offset 0 in the `vmctx` struct
* `bound=vmctx+8`: The pointer to the address to the end of this heap is placed at offset 8 in the `vmctx` struct
* `bound=vmctx+8`: The bound of this heap (size in bytes) is placed at offset 8 in the `vmctx` struct

The `ptr` and `bound` arguments make explicit the placement of the pointers to the start and end of the heap memory in
the environment struct. `vmctx+0` means that at offset 0 of the environment struct there will be the pointer to the start
Expand All @@ -412,11 +412,11 @@ See the diagram below, on how the `vmctx` struct ends up if with multiple heaps:
┌─────────────────────┐ vmctx+0
│heap0: start address │
├─────────────────────┤ vmctx+8
│heap0: end address
│heap0: bound
├─────────────────────┤ vmctx+16
│heap1: start address │
├─────────────────────┤ vmctx+24
│heap1: end address
│heap1: bound
├─────────────────────┤ vmctx+32
│etc... │
└─────────────────────┘
Expand All @@ -443,6 +443,21 @@ block0(v0: i64, v1: i64, v2: i32):
; run: %heap_load_store(0, 1) == 1
```

##### `table` directive

The `table` directive allows a test to request a table to be allocated and passed to the test via the environment struct.

A sample table annotation is the following:
```
; table: count=10, entry_size=8, ptr=vmctx+0, bound=vmctx+8
```

This indicates the following:
* `count=10`: The table should have 10 entries.
* `entry_size=8`: Each entry should have a space of 8 bytes.
* `ptr=vmctx+0`: The pointer to the address to the start of this table is placed at offset 0 in the `vmctx` struct
* `bound=vmctx+8`: The bound of this table (number of entries) is placed at offset 8 in the `vmctx` struct


### `test interpret`

Expand Down
163 changes: 163 additions & 0 deletions cranelift/filetests/filetests/runtests/tables.clif
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
test run
target x86_64
target s390x
target aarch64

function %set_get_i64(i64 vmctx, i64, i64) -> i64 {
gv0 = vmctx
gv1 = load.i64 notrap aligned gv0
gv2 = load.i64 notrap aligned gv0 +8
table0 = dynamic gv1, element_size 8, bound gv2, index_type i64

block0(v0: i64, v1: i64, v2: i64):
v3 = table_addr.i64 table0, v1, +0
store.i64 v2, v3
v4 = load.i64 v3
return v4
}
; table: count=10, entry_size=8, ptr=vmctx+0, bound=vmctx+8
; run: %set_get_i64(0, 1) == 1
; run: %set_get_i64(0, 10) == 10
; run: %set_get_i64(1, 1) == 1
; run: %set_get_i64(1, 0xC0FFEEEE_DECAFFFF) == 0xC0FFEEEE_DECAFFFF
; run: %set_get_i64(10, 1) == 1
; run: %set_get_i64(10, 0xC0FFEEEE_DECAFFFF) == 0xC0FFEEEE_DECAFFFF

function %set_get_i32(i64 vmctx, i64, i32) -> i32 {
gv0 = vmctx
gv1 = load.i64 notrap aligned gv0
gv2 = load.i64 notrap aligned gv0 +8
table0 = dynamic gv1, element_size 8, bound gv2, index_type i64

block0(v0: i64, v1: i64, v2: i32):
;; Note here the offset +4
v3 = table_addr.i64 table0, v1, +4
store.i32 v2, v3
v4 = load.i32 v3
return v4
}
; table: count=10, entry_size=8, ptr=vmctx+0, bound=vmctx+8
; run: %set_get_i32(0, 1) == 1
; run: %set_get_i32(0, 10) == 10
; run: %set_get_i32(1, 1) == 1
; run: %set_get_i32(1, 0xC0FFEEEE) == 0xC0FFEEEE
; run: %set_get_i32(10, 1) == 1
; run: %set_get_i32(10, 0xC0FFEEEE) == 0xC0FFEEEE


function %set_get_i8(i64 vmctx, i64, i8) -> i8 {
gv0 = vmctx
gv1 = load.i64 notrap aligned gv0
gv2 = load.i64 notrap aligned gv0 +8
table0 = dynamic gv1, element_size 1, bound gv2, index_type i64

block0(v0: i64, v1: i64, v2: i8):
v3 = table_addr.i64 table0, v1, +0
store.i8 v2, v3
v4 = load.i8 v3
return v4
}
; table: count=2, entry_size=1, ptr=vmctx+0, bound=vmctx+8
; run: %set_get_i8(0, 1) == 1
; run: %set_get_i8(0, 0xC0) == 0xC0
; run: %set_get_i8(1, 1) == 1
; run: %set_get_i8(1, 0xFF) == 0xFF



function %large_elm_size(i64 vmctx, i64, i64, i8) -> i8 {
gv0 = vmctx
gv1 = load.i64 notrap aligned gv0
gv2 = load.i64 notrap aligned gv0 +8
table0 = dynamic gv1, element_size 10240, bound gv2, index_type i64

block0(v0: i64, v1: i64, v2: i64, v3: i8):
v4 = table_addr.i64 table0, v1, +0
v5 = iadd.i64 v4, v2
store.i8 v3, v5
v6 = load.i8 v5
return v6
}
; table: count=5, entry_size=10240, ptr=vmctx+0, bound=vmctx+8
; run: %large_elm_size(0, 0, 1) == 1
; run: %large_elm_size(1, 0, 0xC0) == 0xC0
; run: %large_elm_size(0, 1, 1) == 1
; run: %large_elm_size(1, 1, 0xFF) == 0xFF
; run: %large_elm_size(0, 127, 1) == 1
; run: %large_elm_size(1, 127, 0xFF) == 0xFF
; run: %large_elm_size(0, 10239, 1) == 1
; run: %large_elm_size(1, 10239, 0xBB) == 0xBB


; Tests writing a i64 which covers 8 table entries at once
; Loads the first byte and the last to confirm that the slots were written
function %multi_elm_write(i64 vmctx, i64, i64) -> i8, i8 {
gv0 = vmctx
gv1 = load.i64 notrap aligned gv0
gv2 = load.i64 notrap aligned gv0 +8
table0 = dynamic gv1, element_size 1, bound gv2, index_type i64

block0(v0: i64, v1: i64, v2: i64):
v3 = table_addr.i64 table0, v1, +0
v4 = table_addr.i64 table0, v1, +7
store.i64 v2, v3
v5 = load.i8 v3
v6 = load.i8 v4
return v5, v6
}
; table: count=16, entry_size=1, ptr=vmctx+0, bound=vmctx+8

;; When writing these test cases keep in mind that s390x is big endian!
;; We just make sure that the first and last byte are the same to deal with that.
; run: %multi_elm_write(0, 0xC0FFEEEE_FFEEEEC0) == [0xC0, 0xC0]
; run: %multi_elm_write(1, 0xAABBCCDD_EEFF00AA) == [0xAA, 0xAA]



; Tests requesting multiple tables and heaps. This is mostly to test the runtest context
; It also tests functions with a bunch of global values and tables and heaps
function %multi_tbl_heap(i64 vmctx, i64, i16) -> i16 {
gv0 = vmctx
gv1 = load.i64 notrap aligned gv0 +0
gv2 = load.i64 notrap aligned gv0 +8
gv3 = load.i64 notrap aligned gv0 +16
gv4 = load.i64 notrap aligned gv0 +24
gv5 = load.i64 notrap aligned gv0 +32
gv6 = load.i64 notrap aligned gv0 +40
gv7 = load.i64 notrap aligned gv0 +48
gv8 = load.i64 notrap aligned gv0 +56
gv9 = load.i64 notrap aligned gv0 +64
gv10 = load.i64 notrap aligned gv0 +72
gv11 = load.i64 notrap aligned gv0 +80
gv12 = load.i64 notrap aligned gv0 +88
gv13 = load.i64 notrap aligned gv0 +96
gv14 = load.i64 notrap aligned gv0 +104

table0 = dynamic gv1, element_size 1, bound gv2, index_type i64
table1 = dynamic gv3, element_size 2, bound gv4, index_type i64
heap0 = static gv5, min 0x10, bound 0x10, offset_guard 0, index_type i64
heap1 = dynamic gv7, bound gv8, offset_guard 0, index_type i64
table2 = dynamic gv9, element_size 3, bound gv10, index_type i64
heap2 = dynamic gv11, bound gv12, offset_guard 0, index_type i64
table3 = dynamic gv13, element_size 3, bound gv14, index_type i64


block0(v0: i64, v1: i64, v2: i16):
v3 = table_addr.i64 table3, v1, +0;
store.i16 v2, v3
v4 = load.i16 v3
return v4
}
; table: count=1, entry_size=1, ptr=vmctx+0, bound=vmctx+8
; table: count=23, entry_size=2, ptr=vmctx+16, bound=vmctx+24
; heap: static, size=0x10, ptr=vmctx+32, bound=vmctx+40
; heap: dynamic, size=0x20, ptr=vmctx+48, bound=vmctx+56
; table: count=9, entry_size=3, ptr=vmctx+64, bound=vmctx+72
; heap: dynamic, size=0x3000, ptr=vmctx+80, bound=vmctx+88
; table: count=9, entry_size=3, ptr=vmctx+96, bound=vmctx+104
; run: %multi_tbl_heap(0, 1) == 1
; run: %multi_tbl_heap(0, 0xC0FF) == 0xC0FF
; run: %multi_tbl_heap(1, 1) == 1
; run: %multi_tbl_heap(1, 0xC0FF) == 0xC0FF
; run: %multi_tbl_heap(9, 1) == 1
; run: %multi_tbl_heap(9, 0xC0FF) == 0xC0FF
102 changes: 79 additions & 23 deletions cranelift/filetests/src/runtest_environment.rs
Original file line number Diff line number Diff line change
@@ -1,58 +1,86 @@
use anyhow::anyhow;
use cranelift_codegen::ir::immediates::Uimm64;
use cranelift_codegen::ir::{ArgumentPurpose, Function};
use cranelift_reader::parse_heap_command;
use cranelift_reader::{Comment, HeapCommand};
use cranelift_reader::{parse_heap_command, parse_table_command};
use cranelift_reader::{Comment, HeapCommand, TableCommand};

#[derive(Debug, Clone)]
pub enum RuntestEntry {
Heap(HeapCommand),
Table(TableCommand),
}

impl RuntestEntry {
/// Tries to parse an entry from a comment, returning None if it isn't possible
pub fn parse_from_comment(comment: &Comment) -> anyhow::Result<Option<Self>> {
if let Some(heap_command) = parse_heap_command(comment.text)? {
return Ok(Some(RuntestEntry::Heap(heap_command)));
}
if let Some(table_command) = parse_table_command(comment.text)? {
return Ok(Some(RuntestEntry::Table(table_command)));
}
Ok(None)
}

pub fn ptr_offset(&self) -> Option<Uimm64> {
match self {
RuntestEntry::Heap(heap) => heap.ptr_offset,
RuntestEntry::Table(table) => table.ptr_offset,
}
}

pub fn bound_offset(&self) -> Option<Uimm64> {
match self {
RuntestEntry::Heap(heap) => heap.bound_offset,
RuntestEntry::Table(table) => table.bound_offset,
}
}
}

/// Stores info about the expected environment for a test function.
#[derive(Debug, Clone)]
pub struct RuntestEnvironment {
pub heaps: Vec<HeapCommand>,
pub entries: Vec<RuntestEntry>,
}

impl RuntestEnvironment {
/// Parse the environment from a set of comments
pub fn parse(comments: &[Comment]) -> anyhow::Result<Self> {
let mut env = RuntestEnvironment { heaps: Vec::new() };
let mut env = RuntestEnvironment {
entries: Vec::new(),
};

// The order of the VMCtx memory is going to be dictated by the order of the comments
// we also enforce the correct vmctx offsets on the comments based on that.
for comment in comments.iter() {
if let Some(heap_command) = parse_heap_command(comment.text)? {
let heap_index = env.heaps.len() as u64;
let expected_ptr = heap_index * 16;
if Some(expected_ptr) != heap_command.ptr_offset.map(|p| p.into()) {
let entry = env.entries.len() as u64;
let expected_ptr = entry * 16;
let expected_bound = (entry * 16) + 8;

if let Some(entry) = RuntestEntry::parse_from_comment(comment)? {
if Some(expected_ptr) != entry.ptr_offset().map(|p| p.into()) {
return Err(anyhow!(
"Invalid ptr offset, expected vmctx+{}",
expected_ptr
));
}

let expected_bound = (heap_index * 16) + 8;
if Some(expected_bound) != heap_command.bound_offset.map(|p| p.into()) {
if Some(expected_bound) != entry.bound_offset().map(|p| p.into()) {
return Err(anyhow!(
"Invalid bound offset, expected vmctx+{}",
expected_bound
));
}

env.heaps.push(heap_command);
env.entries.push(entry);
};
}

Ok(env)
}

pub fn is_active(&self) -> bool {
!self.heaps.is_empty()
}

/// Allocates memory for heaps
pub fn allocate_memory(&self) -> Vec<HeapMemory> {
self.heaps
.iter()
.map(|cmd| {
let size: u64 = cmd.size.into();
vec![0u8; size as usize]
})
.collect()
!self.entries.is_empty()
}

/// Validates the signature of a [Function] ensuring that if this environment is active, the
Expand All @@ -75,6 +103,34 @@ impl RuntestEnvironment {

Ok(())
}

/// Allocates a struct to be injected into the test.
pub fn runtime_struct(
&self,
mut alloc_heap: impl FnMut(u64) -> u64,
mut alloc_table: impl FnMut(u64, u64) -> u64,
) -> Vec<u64> {
let context_struct = self
.entries
.iter()
.flat_map(|entry| match entry {
RuntestEntry::Heap(heap) => {
let size: u64 = heap.size.into();
[alloc_heap(size), size]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a behaviour change from what we had before.

We used to put the start and end pointers. However it looks like that's not whats expected by the bound mechanism for heaps.

This changes it to pass the size in bytes of the heap.

}
RuntestEntry::Table(table) => {
let entry_size: u64 = table.entry_size.into();
let entry_count: u64 = table.entry_count.into();
let bytes = entry_size * entry_count;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is wrong according to docs/ir.md.

Table bounds should be the number of elements and not the number of bytes.

However nothing works when I change it to that. Probably something to investigate further.

See:

The *table bound* is the number of elements currently in the table. This is
the bound that `table_addr` checks against.


[alloc_table(entry_size, entry_count), bytes]
}
})
.collect();

context_struct
}
}

pub(crate) type HeapMemory = Vec<u8>;
pub(crate) type TableMemory = Vec<u8>;
32 changes: 14 additions & 18 deletions cranelift/filetests/src/test_interpret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,28 +82,24 @@ pub fn register_heaps<'a>(
state: &mut InterpreterState<'a>,
test_env: &RuntestEnvironment,
) -> DataValue {
let mem = test_env.allocate_memory();
let vmctx_struct = mem
.into_iter()
// This memory layout (a contiguous list of base + bound ptrs)
// is enforced by the RuntestEnvironment when parsing the heap
// directives. So we are safe to replicate that here.
.flat_map(|mem| {
let heap_len = mem.len() as u64;
let vmctx_struct = test_env.runtime_struct(
|size| {
let mem = vec![0u8; size as usize];
let heap = state.register_heap(HeapInit::FromBacking(mem));
[
state.get_heap_address(I64, heap, 0).unwrap(),
state.get_heap_address(I64, heap, heap_len).unwrap(),
]
})
.map(|addr| {
let addr = state.get_heap_address(I64, heap, 0).unwrap();

let mut mem = [0u8; 8];
addr.write_to_slice(&mut mem[..]);
mem
})
.flatten()
u64::from_ne_bytes(mem)
},
|_size, _count| unimplemented!(),
);

let vmctx_mem = vmctx_struct
.into_iter()
.flat_map(|e| e.to_ne_bytes())
.collect();

let vmctx_heap = state.register_heap(HeapInit::FromBacking(vmctx_struct));
let vmctx_heap = state.register_heap(HeapInit::FromBacking(vmctx_mem));
state.get_heap_address(I64, vmctx_heap, 0).unwrap()
}
Loading