diff --git a/src/ast.rs b/src/ast.rs index a3df0d8..2bcabfb 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,4 +1,4 @@ -use crate::instructions::Instruction; +use crate::interpreter::Instruction; use std::str; #[derive(PartialEq, Debug)] diff --git a/src/decoders/binary.rs b/src/decoders/binary.rs index 0c77e52..9a1d77e 100644 --- a/src/decoders/binary.rs +++ b/src/decoders/binary.rs @@ -1,4 +1,4 @@ -use crate::{ast::*, instructions::Instruction}; +use crate::{ast::*, interpreter::Instruction}; use nom::{ error::{make_error, ErrorKind, ParseError}, Err, IResult, diff --git a/src/encoders/wat.rs b/src/encoders/wat.rs index 65684d8..89387ed 100644 --- a/src/encoders/wat.rs +++ b/src/encoders/wat.rs @@ -1,6 +1,6 @@ use crate::{ ast::{Adapter, Export, Forward, ImportedFunction, InterfaceType, Interfaces, Type}, - instructions::Instruction, + interpreter::Instruction, }; impl From<&InterfaceType> for String { @@ -270,7 +270,7 @@ impl<'input> From<&Interfaces<'input>> for String { #[cfg(test)] mod tests { - use crate::{ast::*, instructions::Instruction}; + use crate::{ast::*, interpreter::Instruction}; #[test] fn test_interface_types() { diff --git a/src/instructions/interpreter.rs b/src/instructions/interpreter.rs deleted file mode 100644 index 79466d7..0000000 --- a/src/instructions/interpreter.rs +++ /dev/null @@ -1,1033 +0,0 @@ -use crate::instructions::{ - stack::{Stack, Stackable}, - wasm::{self, FunctionIndex, InterfaceType, InterfaceValue, TypedIndex}, - Instruction, -}; -use std::{ - cell::Cell, - convert::{TryFrom, TryInto}, - marker::PhantomData, -}; - -struct Runtime<'invocation, 'instance, Instance, Export, LocalImport, Memory> -where - Export: wasm::Export + 'instance, - LocalImport: wasm::LocalImport + 'instance, - Memory: wasm::Memory + 'instance, - Instance: wasm::Instance + 'instance, -{ - invocation_inputs: &'invocation [InterfaceValue], - stack: Stack, - wasm_instance: &'instance Instance, - _wasm_exports: PhantomData, - _wasm_locals_or_imports: PhantomData, - _wasm_memories: PhantomData, -} - -type ExecutableInstruction = - Box) -> Result<(), String>>; - -pub struct Interpreter -where - Export: wasm::Export, - LocalImport: wasm::LocalImport, - Memory: wasm::Memory, - Instance: wasm::Instance, -{ - executable_instructions: Vec>, -} - -impl Interpreter -where - Export: wasm::Export, - LocalImport: wasm::LocalImport, - Memory: wasm::Memory, - Instance: wasm::Instance, -{ - fn iter( - &self, - ) -> impl Iterator> + '_ - { - self.executable_instructions.iter() - } - - pub fn run( - &self, - invocation_inputs: &[InterfaceValue], - wasm_instance: &Instance, - ) -> Result, String> { - let mut runtime = Runtime { - invocation_inputs, - stack: Stack::new(), - wasm_instance, - _wasm_exports: PhantomData, - _wasm_locals_or_imports: PhantomData, - _wasm_memories: PhantomData, - }; - - for executable_instruction in self.iter() { - match executable_instruction(&mut runtime) { - Ok(_) => continue, - Err(message) => return Err(message), - } - } - - Ok(runtime.stack) - } -} - -impl<'binary_input, Instance, Export, LocalImport, Memory> TryFrom<&Vec>> - for Interpreter -where - Export: wasm::Export, - LocalImport: wasm::LocalImport, - Memory: wasm::Memory, - Instance: wasm::Instance, -{ - type Error = String; - - fn try_from(instructions: &Vec) -> Result { - let executable_instructions = instructions - .iter() - .map( - |instruction| -> ExecutableInstruction { - match instruction { - Instruction::ArgumentGet { index } => { - let index = index.to_owned(); - let instruction_name: String = instruction.into(); - - Box::new(move |runtime: &mut Runtime| -> Result<(), _> { - let invocation_inputs = runtime.invocation_inputs; - - if index >= (invocation_inputs.len() as u64) { - return Err(format!( - "`{}` cannot access argument #{} because it doesn't exist.", - instruction_name, index - )); - } - - runtime.stack.push(invocation_inputs[index as usize].clone()); - - Ok(()) - }) - } - Instruction::Call { function_index: index } => { - let index = index.to_owned(); - let instruction_name: String = instruction.into(); - - Box::new(move |runtime: &mut Runtime| -> Result<(), _> { - let instance = runtime.wasm_instance; - let function_index = FunctionIndex::new(index); - - match instance.local_or_import(function_index) { - Some(local_or_import) => { - let inputs_cardinality = local_or_import.inputs_cardinality(); - - match runtime.stack.pop(inputs_cardinality) { - Some(inputs) => { - let input_types = inputs - .iter() - .map(|input| input.into()) - .collect::>(); - - if input_types != local_or_import.inputs() { - return Err(format!( - "`{}` cannot call the local or imported function `{}` because the value types on the stack mismatch the function signature (expects {:?}).", - instruction_name, - index, - local_or_import.inputs(), - )) - } - - match local_or_import.call(&inputs) { - Ok(outputs) => { - for output in outputs.iter() { - runtime.stack.push(output.clone()); - } - - Ok(()) - } - Err(_) => Err(format!( - "`{}` failed when calling the local or imported function `{}`.", - instruction_name, - index - )) - } - } - None => Err(format!( - "`{}` cannot call the local or imported function `{}` because there is not enough data on the stack for the arguments (needs {}).", - instruction_name, - index, - inputs_cardinality, - )) - } - } - None => Err(format!( - "`{}` cannot call the local or imported function `{}` because it doesn't exist.", - instruction_name, - index, - )) - } - }) - } - Instruction::CallExport { export_name } => { - let export_name = (*export_name).to_owned(); - let instruction_name: String = instruction.into(); - - Box::new(move |runtime: &mut Runtime| -> Result<(), _> { - let instance = runtime.wasm_instance; - - match instance.export(&export_name) { - Some(export) => { - let inputs_cardinality = export.inputs_cardinality(); - - match runtime.stack.pop(inputs_cardinality) { - Some(inputs) => { - let input_types = inputs - .iter() - .map(|input| input.into()) - .collect::>(); - - if input_types != export.inputs() { - return Err(format!( - "`{}` cannot call the exported function `{}` because the value types on the stack mismatch the function signature (expects {:?}).", - instruction_name, - export_name, - export.inputs(), - )) - } - - match export.call(&inputs) { - Ok(outputs) => { - for output in outputs.iter() { - runtime.stack.push(output.clone()); - } - - Ok(()) - } - Err(_) => Err(format!( - "`{}` failed when calling the exported function `{}`.", - instruction_name, - export_name - )) - } - } - None => Err(format!( - "`{}` cannot call the exported function `{}` because there is not enough data on the stack for the arguments (needs {}).", - instruction_name, - export_name, - inputs_cardinality, - )) - } - } - None => Err(format!( - "`{}` cannot call the exported function `{}` because it doesn't exist.", - instruction_name, - export_name, - )) - } - }) - } - Instruction::ReadUtf8 => { - let instruction_name: String = instruction.into(); - - Box::new(move |runtime: &mut Runtime| -> Result<(), _> { - match runtime.stack.pop(2) { - Some(inputs) => match runtime.wasm_instance.memory(0) { - Some(memory) => { - let length = i32::try_from(&inputs[0])? as usize; - let pointer = i32::try_from(&inputs[1])? as usize; - let memory_view = memory.view::(); - - if memory_view.len() < pointer + length { - return Err(format!( - "`{}` failed because it has to read out of the memory bounds (index {} > memory length {}).", - instruction_name, - pointer + length, - memory_view.len() - )); - } - - let data: Vec = (&memory_view[pointer..pointer + length]) - .iter() - .map(Cell::get) - .collect(); - - match String::from_utf8(data) { - Ok(string) => { - runtime.stack.push(InterfaceValue::String(string)); - - Ok(()) - } - Err(utf8_error) => Err(format!( - "`{}` failed because the read string isn't UTF-8 valid ({}).", - instruction_name, - utf8_error, - )) - } - } - None => Err(format!( - "`{}` failed because there is no memory to read.", - instruction_name - )) - } - None => Err(format!( - "`{}` failed because there is not enough data on the stack (needs 2).", - instruction_name, - )) - } - }) - } - Instruction::WriteUtf8 { allocator_name } => { - let allocator_name = (*allocator_name).to_owned(); - let instruction_name: String = instruction.into(); - - Box::new(move |runtime: &mut Runtime| -> Result<(), _> { - let instance = runtime.wasm_instance; - - match instance.export(&allocator_name) { - Some(allocator) => { - if allocator.inputs() != [InterfaceType::I32] || - allocator.outputs() != [InterfaceType::I32] { - return Err(format!( - "`{}` failed because the allocator `{}` has an invalid signature (expects [I32] -> [I32]).", - instruction_name, - allocator_name, - )) - } - - match runtime.wasm_instance.memory(0) { - Some(memory) => match runtime.stack.pop1() { - Some(string) => { - let memory_view = memory.view::(); - - let string: String = (&string).try_into()?; - let string_bytes = string.as_bytes(); - let string_length = (string_bytes.len() as i32) - .try_into() - .map_err(|error| format!("{}", error))?; - - match allocator.call(&[InterfaceValue::I32(string_length)]) { - Ok(outputs) => { - let string_pointer: i32 = (&outputs[0]).try_into()?; - - for (nth, byte) in string_bytes.iter().enumerate() { - memory_view[string_pointer as usize + nth].set(*byte); - } - - runtime.stack.push(InterfaceValue::I32(string_pointer)); - runtime.stack.push(InterfaceValue::I32(string_length)); - - Ok(()) - } - Err(_) => Err(format!( - "`{}` failed when calling the allocator `{}`.", - instruction_name, - allocator_name, - )) - } - } - None => Err(format!( - "`{}` cannot call the allocator `{}` because there is not enough data on the stack for the arguments (needs {}).", - instruction_name, - allocator_name, - 1 - )) - } - None => Err(format!( - "`{}` failed because there is no memory to write into.", - instruction_name - )) - } - } - None => Err(format!( - "`{}` failed because the exported function `{}` (the allocator) doesn't exist.", - instruction_name, - allocator_name - )) - } - }) - } - _ => unimplemented!(), - } - }, - ) - .collect(); - - Ok(Interpreter { - executable_instructions, - }) - } -} - -#[cfg(test)] -mod tests { - use super::Interpreter; - use crate::instructions::{ - stack::Stackable, - wasm::{self, InterfaceType, InterfaceValue}, - Instruction, - }; - use std::{cell::Cell, collections::HashMap, convert::TryInto}; - - struct Export { - inputs: Vec, - outputs: Vec, - function: fn(arguments: &[InterfaceValue]) -> Result, ()>, - } - - impl wasm::Export for Export { - fn inputs_cardinality(&self) -> usize { - self.inputs.len() as usize - } - - fn outputs_cardinality(&self) -> usize { - self.outputs.len() - } - - fn inputs(&self) -> &[InterfaceType] { - &self.inputs - } - - fn outputs(&self) -> &[InterfaceType] { - &self.outputs - } - - fn call(&self, arguments: &[InterfaceValue]) -> Result, ()> { - (self.function)(arguments) - } - } - - struct LocalImport { - inputs: Vec, - outputs: Vec, - function: fn(arguments: &[InterfaceValue]) -> Result, ()>, - } - - impl wasm::LocalImport for LocalImport { - fn inputs_cardinality(&self) -> usize { - self.inputs.len() as usize - } - - fn outputs_cardinality(&self) -> usize { - self.outputs.len() - } - - fn inputs(&self) -> &[InterfaceType] { - &self.inputs - } - - fn outputs(&self) -> &[InterfaceType] { - &self.outputs - } - - fn call(&self, arguments: &[InterfaceValue]) -> Result, ()> { - (self.function)(arguments) - } - } - - #[derive(Default)] - struct Memory { - data: Vec>, - } - - impl Memory { - fn new(data: Vec>) -> Self { - Self { data } - } - } - - impl wasm::Memory for Memory { - fn view(&self) -> &[Cell] { - let slice = self.data.as_slice(); - - unsafe { ::std::slice::from_raw_parts(slice.as_ptr() as *const Cell, slice.len()) } - } - } - - #[derive(Default)] - struct Instance { - exports: HashMap, - locals_or_imports: HashMap, - memory: Memory, - } - - impl Instance { - fn new() -> Self { - Self { - exports: { - let mut hashmap = HashMap::new(); - hashmap.insert( - "sum".into(), - Export { - inputs: vec![InterfaceType::I32, InterfaceType::I32], - outputs: vec![InterfaceType::I32], - function: |arguments: &[InterfaceValue]| { - let a: i32 = (&arguments[0]).try_into().unwrap(); - let b: i32 = (&arguments[1]).try_into().unwrap(); - - Ok(vec![InterfaceValue::I32(a + b)]) - }, - }, - ); - hashmap.insert( - "alloc".into(), - Export { - inputs: vec![InterfaceType::I32], - outputs: vec![InterfaceType::I32], - function: |arguments: &[InterfaceValue]| { - let _size: i32 = (&arguments[0]).try_into().unwrap(); - - Ok(vec![InterfaceValue::I32(0)]) - }, - }, - ); - - hashmap - }, - locals_or_imports: { - let mut hashmap = HashMap::new(); - hashmap.insert( - 42, - LocalImport { - inputs: vec![InterfaceType::I32, InterfaceType::I32], - outputs: vec![InterfaceType::I32], - function: |arguments: &[InterfaceValue]| { - let a: i32 = (&arguments[0]).try_into().unwrap(); - let b: i32 = (&arguments[1]).try_into().unwrap(); - - Ok(vec![InterfaceValue::I32(a * b)]) - }, - }, - ); - - hashmap - }, - memory: Memory::new(vec![Cell::new(0); 128]), - } - } - } - - impl wasm::Instance for Instance { - fn export(&self, export_name: &str) -> Option<&Export> { - self.exports.get(export_name) - } - - fn local_or_import( - &self, - index: I, - ) -> Option<&LocalImport> { - self.locals_or_imports.get(&index.index()) - } - - fn memory(&self, _index: usize) -> Option<&Memory> { - Some(&self.memory) - } - } - - #[test] - fn test_interpreter_from_instructions() { - let instructions = vec![ - Instruction::ArgumentGet { index: 0 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::CallExport { export_name: "foo" }, - Instruction::ReadUtf8, - Instruction::Call { function_index: 7 }, - ]; - let interpreter: Interpreter<(), (), (), ()> = (&instructions).try_into().unwrap(); - - assert_eq!(interpreter.executable_instructions.len(), 5); - } - - macro_rules! test { - ( - $test_name:ident = - instructions: [ $($instructions:expr),* $(,)* ], - invocation_inputs: [ $($invocation_inputs:expr),* $(,)* ], - instance: $instance:expr, - stack: [ $($stack:expr),* $(,)* ] - $(,)* - ) => { - #[test] - #[allow(non_snake_case)] - fn $test_name() { - let interpreter: Interpreter = - (&vec![$($instructions),*]).try_into().unwrap(); - - let invocation_inputs = vec![$($invocation_inputs),*]; - let instance = $instance; - let run = interpreter.run(&invocation_inputs, &instance); - - assert!(run.is_ok()); - - let stack = run.unwrap(); - - assert_eq!(stack.as_slice(), &[$($stack),*]); - } - }; - - ( - $test_name:ident = - instructions: [ $($instructions:expr),* $(,)* ], - invocation_inputs: [ $($invocation_inputs:expr),* $(,)* ], - instance: $instance:expr, - error: $error:expr - $(,)* - ) => { - #[test] - #[allow(non_snake_case)] - fn $test_name() { - let interpreter: Interpreter = - (&vec![$($instructions),*]).try_into().unwrap(); - - let invocation_inputs = vec![$($invocation_inputs),*]; - let instance = $instance; - let run = interpreter.run(&invocation_inputs, &instance); - - assert!(run.is_err()); - - let error = run.unwrap_err(); - - assert_eq!(error, String::from($error)); - } - }; - } - - test!( - test_interpreter_argument_get = - instructions: [Instruction::ArgumentGet { index: 0 }], - invocation_inputs: [InterfaceValue::I32(42)], - instance: Instance::new(), - stack: [InterfaceValue::I32(42)], - ); - - test!( - test_interpreter_argument_get__twice = - instructions: [ - Instruction::ArgumentGet { index: 0 }, - Instruction::ArgumentGet { index: 1 }, - ], - invocation_inputs: [ - InterfaceValue::I32(7), - InterfaceValue::I32(42), - ], - instance: Instance::new(), - stack: [ - InterfaceValue::I32(7), - InterfaceValue::I32(42), - ], - ); - - test!( - test_interpreter_argument_get__invalid_index = - instructions: [Instruction::ArgumentGet { index: 1 }], - invocation_inputs: [InterfaceValue::I32(42)], - instance: Instance::new(), - error: "`arg.get 1` cannot access argument #1 because it doesn't exist." - ); - - test!( - test_interpreter_call_export = - instructions: [ - Instruction::ArgumentGet { index: 1 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::CallExport { export_name: "sum" }, - ], - invocation_inputs: [ - InterfaceValue::I32(3), - InterfaceValue::I32(4), - ], - instance: Instance::new(), - stack: [InterfaceValue::I32(7)], - ); - - test!( - test_interpreter_call_export__invalid_export_name = - instructions: [Instruction::CallExport { export_name: "bar" }], - invocation_inputs: [], - instance: Instance::new(), - error: r#"`call-export "bar"` cannot call the exported function `bar` because it doesn't exist."#, - ); - - test!( - test_interpreter_call_export__stack_is_too_small = - instructions: [ - Instruction::ArgumentGet { index: 0 }, - Instruction::CallExport { export_name: "sum" }, - ], - invocation_inputs: [ - InterfaceValue::I32(3), - InterfaceValue::I32(4), - ], - instance: Instance::new(), - error: r#"`call-export "sum"` cannot call the exported function `sum` because there is not enough data on the stack for the arguments (needs 2)."#, - ); - - test!( - test_interpreter_call_export__invalid_types_in_the_stack = - instructions: [ - Instruction::ArgumentGet { index: 1 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::CallExport { export_name: "sum" }, - ], - invocation_inputs: [ - InterfaceValue::I32(3), - InterfaceValue::I64(4), - // ^^^ mismatch with `sum` signature - ], - instance: Instance::new(), - error: r#"`call-export "sum"` cannot call the exported function `sum` because the value types on the stack mismatch the function signature (expects [I32, I32])."#, - ); - - test!( - test_interpreter_call_export__failure_when_calling = - instructions: [ - Instruction::ArgumentGet { index: 1 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::CallExport { export_name: "sum" }, - ], - invocation_inputs: [ - InterfaceValue::I32(3), - InterfaceValue::I32(4), - ], - instance: Instance { - exports: { - let mut hashmap = HashMap::new(); - hashmap.insert( - "sum".into(), - Export { - inputs: vec![InterfaceType::I32, InterfaceType::I32], - outputs: vec![InterfaceType::I32], - function: |_| Err(()), - // ^^^^^^^ function fails - }, - ); - - hashmap - }, - ..Default::default() - }, - error: r#"`call-export "sum"` failed when calling the exported function `sum`."#, - ); - - test!( - test_interpreter_call_export__void = - instructions: [ - Instruction::ArgumentGet { index: 1 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::CallExport { export_name: "sum" }, - ], - invocation_inputs: [ - InterfaceValue::I32(3), - InterfaceValue::I32(4), - ], - instance: Instance { - exports: { - let mut hashmap = HashMap::new(); - hashmap.insert( - "sum".into(), - Export { - inputs: vec![InterfaceType::I32, InterfaceType::I32], - outputs: vec![InterfaceType::I32], - function: |_| Ok(vec![]), - // ^^^^^^^^^^ void function - }, - ); - - hashmap - }, - ..Default::default() - }, - stack: [], - ); - - test!( - test_interpreter_read_utf8 = - instructions: [ - Instruction::ArgumentGet { index: 1 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::ReadUtf8, - ], - invocation_inputs: [ - InterfaceValue::I32(13), - // ^^^^^^^ length - InterfaceValue::I32(0), - // ^^^^^^ pointer - ], - instance: Instance { - memory: Memory::new("Hello, World!".as_bytes().iter().map(|u| Cell::new(*u)).collect()), - ..Default::default() - }, - stack: [InterfaceValue::String("Hello, World!".into())], - ); - - test!( - test_interpreter_read_utf8__read_out_of_memory = - instructions: [ - Instruction::ArgumentGet { index: 1 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::ReadUtf8, - ], - invocation_inputs: [ - InterfaceValue::I32(13), - // ^^^^^^^ length is too long - InterfaceValue::I32(0), - // ^^^^^^ pointer - ], - instance: Instance { - memory: Memory::new("Hello!".as_bytes().iter().map(|u| Cell::new(*u)).collect()), - ..Default::default() - }, - error: r#"`read-utf8` failed because it has to read out of the memory bounds (index 13 > memory length 6)."#, - ); - - test!( - test_interpreter_read_utf8__invalid_encoding = - instructions: [ - Instruction::ArgumentGet { index: 1 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::ReadUtf8, - ], - invocation_inputs: [ - InterfaceValue::I32(4), - // ^^^^^^ length is too long - InterfaceValue::I32(0), - // ^^^^^^ pointer - ], - instance: Instance { - memory: Memory::new(vec![0, 159, 146, 150].iter().map(|b| Cell::new(*b)).collect::>>()), - ..Default::default() - }, - error: r#"`read-utf8` failed because the read string isn't UTF-8 valid (invalid utf-8 sequence of 1 bytes from index 1)."#, - ); - - test!( - test_interpreter_read_utf8__stack_is_too_small = - instructions: [ - Instruction::ArgumentGet { index: 0 }, - Instruction::ReadUtf8, - // ^^^^^^^^ `read-utf8` expects 2 values on the stack, only one is present. - ], - invocation_inputs: [ - InterfaceValue::I32(13), - InterfaceValue::I32(0), - ], - instance: Instance::new(), - error: r#"`read-utf8` failed because there is not enough data on the stack (needs 2)."#, - ); - - test!( - test_interpreter_call = - instructions: [ - Instruction::ArgumentGet { index: 1 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::Call { function_index: 42 }, - ], - invocation_inputs: [ - InterfaceValue::I32(3), - InterfaceValue::I32(4), - ], - instance: Instance::new(), - stack: [InterfaceValue::I32(12)], - ); - - test!( - test_interpreter_call__invalid_local_import_index = - instructions: [ - Instruction::Call { function_index: 42 }, - ], - invocation_inputs: [ - InterfaceValue::I32(3), - InterfaceValue::I32(4), - ], - instance: Instance { ..Default::default() }, - error: r#"`call 42` cannot call the local or imported function `42` because it doesn't exist."#, - ); - - test!( - test_interpreter_call__stack_is_too_small = - instructions: [ - Instruction::ArgumentGet { index: 0 }, - Instruction::Call { function_index: 42 }, - // ^^ `42` expects 2 values on the stack, only one is present - ], - invocation_inputs: [ - InterfaceValue::I32(3), - InterfaceValue::I32(4), - ], - instance: Instance::new(), - error: r#"`call 42` cannot call the local or imported function `42` because there is not enough data on the stack for the arguments (needs 2)."#, - ); - - test!( - test_interpreter_call__invalid_types_in_the_stack = - instructions: [ - Instruction::ArgumentGet { index: 1 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::Call { function_index: 42 }, - ], - invocation_inputs: [ - InterfaceValue::I32(3), - InterfaceValue::I64(4), - // ^^^ mismatch with `42` signature - ], - instance: Instance::new(), - error: r#"`call 42` cannot call the local or imported function `42` because the value types on the stack mismatch the function signature (expects [I32, I32])."#, - ); - - test!( - test_interpreter_call__failure_when_calling = - instructions: [ - Instruction::ArgumentGet { index: 1 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::Call { function_index: 42 }, - ], - invocation_inputs: [ - InterfaceValue::I32(3), - InterfaceValue::I32(4), - ], - instance: Instance { - locals_or_imports: { - let mut hashmap = HashMap::new(); - hashmap.insert( - 42, - LocalImport { - inputs: vec![InterfaceType::I32, InterfaceType::I32], - outputs: vec![InterfaceType::I32], - function: |_| Err(()), - // ^^^^^^^ function fails - }, - ); - - hashmap - }, - ..Default::default() - }, - error: r#"`call 42` failed when calling the local or imported function `42`."#, - ); - - test!( - test_interpreter_call__void = - instructions: [ - Instruction::ArgumentGet { index: 1 }, - Instruction::ArgumentGet { index: 0 }, - Instruction::Call { function_index: 42 }, - ], - invocation_inputs: [ - InterfaceValue::I32(3), - InterfaceValue::I32(4), - ], - instance: Instance { - locals_or_imports: { - let mut hashmap = HashMap::new(); - hashmap.insert( - 42, - LocalImport { - inputs: vec![InterfaceType::I32, InterfaceType::I32], - outputs: vec![InterfaceType::I32], - function: |_| Ok(vec![]), - // ^^^^^^^^^^ void fails - }, - ); - - hashmap - }, - ..Default::default() - }, - stack: [], - ); - - test!( - test_interpreter_write_utf8 = - instructions: [ - Instruction::ArgumentGet { index: 0 }, - Instruction::WriteUtf8 { allocator_name: "alloc" }, - ], - invocation_inputs: [InterfaceValue::String("Hello, World!".into())], - instance: Instance::new(), - stack: [ - InterfaceValue::I32(0), - // ^^^^^^ pointer - InterfaceValue::I32(13), - // ^^^^^^^ length - ] - ); - - test!( - test_interpreter_write_utf8__roundtrip_with_read_utf8 = - instructions: [ - Instruction::ArgumentGet { index: 0 }, - Instruction::WriteUtf8 { allocator_name: "alloc" }, - Instruction::ReadUtf8, - ], - invocation_inputs: [InterfaceValue::String("Hello, World!".into())], - instance: Instance::new(), - stack: [InterfaceValue::String("Hello, World!".into())], - ); - - test!( - test_interpreter_write_utf8__allocator_does_not_exist = - instructions: [Instruction::WriteUtf8 { allocator_name: "alloc" }], - invocation_inputs: [], - instance: Instance { ..Default::default() }, - error: r#"`write-utf8 "alloc"` failed because the exported function `alloc` (the allocator) doesn't exist."#, - ); - - test!( - test_interpreter_write_utf8__stack_is_too_small = - instructions: [ - Instruction::WriteUtf8 { allocator_name: "alloc" } - // ^^^^^ `alloc` expects 1 value on the stack, none is present - ], - invocation_inputs: [InterfaceValue::String("Hello, World!".into())], - instance: Instance::new(), - error: r#"`write-utf8 "alloc"` cannot call the allocator `alloc` because there is not enough data on the stack for the arguments (needs 1)."#, - ); - - test!( - test_interpreter_write_utf8__failure_when_calling_the_allocator = - instructions: [ - Instruction::ArgumentGet { index: 0 }, - Instruction::WriteUtf8 { allocator_name: "alloc-fail" } - ], - invocation_inputs: [InterfaceValue::String("Hello, World!".into())], - instance: { - let mut instance = Instance::new(); - instance.exports.insert( - "alloc-fail".into(), - Export { - inputs: vec![InterfaceType::I32], - outputs: vec![InterfaceType::I32], - function: |_| Err(()), - // ^^^^^^^ function fails - }, - ); - - instance - }, - error: r#"`write-utf8 "alloc-fail"` failed when calling the allocator `alloc-fail`."#, - ); - - test!( - test_interpreter_write_utf8__invalid_allocator_signature = - instructions: [ - Instruction::ArgumentGet { index: 0 }, - Instruction::WriteUtf8 { allocator_name: "alloc-fail" } - ], - invocation_inputs: [InterfaceValue::String("Hello, World!".into())], - instance: { - let mut instance = Instance::new(); - instance.exports.insert( - "alloc-fail".into(), - Export { - inputs: vec![InterfaceType::I32, InterfaceType::I32], - outputs: vec![], - function: |_| Err(()), - }, - ); - - instance - }, - error: r#"`write-utf8 "alloc-fail"` failed because the allocator `alloc-fail` has an invalid signature (expects [I32] -> [I32])."#, - ); -} diff --git a/src/instructions/mod.rs b/src/interpreter/instruction.rs similarity index 93% rename from src/instructions/mod.rs rename to src/interpreter/instruction.rs index 173e4af..e474d2d 100644 --- a/src/instructions/mod.rs +++ b/src/interpreter/instruction.rs @@ -1,9 +1,5 @@ use crate::ast::InterfaceType; -pub mod interpreter; -mod stack; -pub mod wasm; - #[derive(PartialEq, Debug)] pub enum Instruction<'input> { ArgumentGet { index: u64 }, diff --git a/src/interpreter/instructions/argument_get.rs b/src/interpreter/instructions/argument_get.rs new file mode 100644 index 0000000..8819302 --- /dev/null +++ b/src/interpreter/instructions/argument_get.rs @@ -0,0 +1,54 @@ +executable_instruction!( + argument_get(index: u64, instruction_name: String) -> _ { + Box::new(move |runtime| -> _ { + let invocation_inputs = runtime.invocation_inputs; + + if index >= (invocation_inputs.len() as u64) { + return Err(format!( + "`{}` cannot access argument #{} because it doesn't exist.", + instruction_name, index + )); + } + + runtime.stack.push(invocation_inputs[index as usize].clone()); + + Ok(()) + }) + } +); + +#[cfg(test)] +mod tests { + test_executable_instruction!( + test_argument_get = + instructions: [Instruction::ArgumentGet { index: 0 }], + invocation_inputs: [InterfaceValue::I32(42)], + instance: Instance::new(), + stack: [InterfaceValue::I32(42)], + ); + + test_executable_instruction!( + test_argument_get__twice = + instructions: [ + Instruction::ArgumentGet { index: 0 }, + Instruction::ArgumentGet { index: 1 }, + ], + invocation_inputs: [ + InterfaceValue::I32(7), + InterfaceValue::I32(42), + ], + instance: Instance::new(), + stack: [ + InterfaceValue::I32(7), + InterfaceValue::I32(42), + ], + ); + + test_executable_instruction!( + test_argument_get__invalid_index = + instructions: [Instruction::ArgumentGet { index: 1 }], + invocation_inputs: [InterfaceValue::I32(42)], + instance: Instance::new(), + error: "`arg.get 1` cannot access argument #1 because it doesn't exist." + ); +} diff --git a/src/interpreter/instructions/call.rs b/src/interpreter/instructions/call.rs new file mode 100644 index 0000000..4a74655 --- /dev/null +++ b/src/interpreter/instructions/call.rs @@ -0,0 +1,187 @@ +use crate::interpreter::wasm::{ + structures::{FunctionIndex, TypedIndex}, + values::InterfaceType, +}; + +executable_instruction!( + call(function_index: usize, instruction_name: String) -> _ { + Box::new(move |runtime| -> _ { + let instance = runtime.wasm_instance; + let index = FunctionIndex::new(function_index); + + match instance.local_or_import(index) { + Some(local_or_import) => { + let inputs_cardinality = local_or_import.inputs_cardinality(); + + match runtime.stack.pop(inputs_cardinality) { + Some(inputs) => { + let input_types = inputs + .iter() + .map(|input| input.into()) + .collect::>(); + + if input_types != local_or_import.inputs() { + return Err(format!( + "`{}` cannot call the local or imported function `{}` because the value types on the stack mismatch the function signature (expects {:?}).", + instruction_name, + function_index, + local_or_import.inputs(), + )) + } + + match local_or_import.call(&inputs) { + Ok(outputs) => { + for output in outputs.iter() { + runtime.stack.push(output.clone()); + } + + Ok(()) + } + Err(_) => Err(format!( + "`{}` failed when calling the local or imported function `{}`.", + instruction_name, + function_index + )) + } + } + None => Err(format!( + "`{}` cannot call the local or imported function `{}` because there is not enough data on the stack for the arguments (needs {}).", + instruction_name, + function_index, + inputs_cardinality, + )) + } + } + None => Err(format!( + "`{}` cannot call the local or imported function `{}` because it doesn't exist.", + instruction_name, + function_index, + )) + } + }) + } +); + +#[cfg(test)] +mod tests { + test_executable_instruction!( + test_call = + instructions: [ + Instruction::ArgumentGet { index: 1 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::Call { function_index: 42 }, + ], + invocation_inputs: [ + InterfaceValue::I32(3), + InterfaceValue::I32(4), + ], + instance: Instance::new(), + stack: [InterfaceValue::I32(12)], + ); + + test_executable_instruction!( + test_call__invalid_local_import_index = + instructions: [ + Instruction::Call { function_index: 42 }, + ], + invocation_inputs: [ + InterfaceValue::I32(3), + InterfaceValue::I32(4), + ], + instance: Instance { ..Default::default() }, + error: r#"`call 42` cannot call the local or imported function `42` because it doesn't exist."#, + ); + + test_executable_instruction!( + test_call__stack_is_too_small = + instructions: [ + Instruction::ArgumentGet { index: 0 }, + Instruction::Call { function_index: 42 }, + // ^^ `42` expects 2 values on the stack, only one is present + ], + invocation_inputs: [ + InterfaceValue::I32(3), + InterfaceValue::I32(4), + ], + instance: Instance::new(), + error: r#"`call 42` cannot call the local or imported function `42` because there is not enough data on the stack for the arguments (needs 2)."#, + ); + + test_executable_instruction!( + test_call__invalid_types_in_the_stack = + instructions: [ + Instruction::ArgumentGet { index: 1 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::Call { function_index: 42 }, + ], + invocation_inputs: [ + InterfaceValue::I32(3), + InterfaceValue::I64(4), + // ^^^ mismatch with `42` signature + ], + instance: Instance::new(), + error: r#"`call 42` cannot call the local or imported function `42` because the value types on the stack mismatch the function signature (expects [I32, I32])."#, + ); + + test_executable_instruction!( + test_call__failure_when_calling = + instructions: [ + Instruction::ArgumentGet { index: 1 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::Call { function_index: 42 }, + ], + invocation_inputs: [ + InterfaceValue::I32(3), + InterfaceValue::I32(4), + ], + instance: Instance { + locals_or_imports: { + let mut hashmap = HashMap::new(); + hashmap.insert( + 42, + LocalImport { + inputs: vec![InterfaceType::I32, InterfaceType::I32], + outputs: vec![InterfaceType::I32], + function: |_| Err(()), + // ^^^^^^^ function fails + }, + ); + + hashmap + }, + ..Default::default() + }, + error: r#"`call 42` failed when calling the local or imported function `42`."#, + ); + + test_executable_instruction!( + test_call__void = + instructions: [ + Instruction::ArgumentGet { index: 1 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::Call { function_index: 42 }, + ], + invocation_inputs: [ + InterfaceValue::I32(3), + InterfaceValue::I32(4), + ], + instance: Instance { + locals_or_imports: { + let mut hashmap = HashMap::new(); + hashmap.insert( + 42, + LocalImport { + inputs: vec![InterfaceType::I32, InterfaceType::I32], + outputs: vec![InterfaceType::I32], + function: |_| Ok(vec![]), + // ^^^^^^^^^^ void fails + }, + ); + + hashmap + }, + ..Default::default() + }, + stack: [], + ); +} diff --git a/src/interpreter/instructions/call_export.rs b/src/interpreter/instructions/call_export.rs new file mode 100644 index 0000000..05de647 --- /dev/null +++ b/src/interpreter/instructions/call_export.rs @@ -0,0 +1,177 @@ +use crate::interpreter::wasm::values::InterfaceType; + +executable_instruction!( + call_export(export_name: String, instruction_name: String) -> _ { + Box::new(move |runtime| -> _ { + let instance = runtime.wasm_instance; + + match instance.export(&export_name) { + Some(export) => { + let inputs_cardinality = export.inputs_cardinality(); + + match runtime.stack.pop(inputs_cardinality) { + Some(inputs) => { + let input_types = inputs + .iter() + .map(|input| input.into()) + .collect::>(); + + if input_types != export.inputs() { + return Err(format!( + "`{}` cannot call the exported function `{}` because the value types on the stack mismatch the function signature (expects {:?}).", + instruction_name, + export_name, + export.inputs(), + )) + } + + match export.call(&inputs) { + Ok(outputs) => { + for output in outputs.iter() { + runtime.stack.push(output.clone()); + } + + Ok(()) + } + Err(_) => Err(format!( + "`{}` failed when calling the exported function `{}`.", + instruction_name, + export_name + )) + } + } + None => Err(format!( + "`{}` cannot call the exported function `{}` because there is not enough data on the stack for the arguments (needs {}).", + instruction_name, + export_name, + inputs_cardinality, + )) + } + } + None => Err(format!( + "`{}` cannot call the exported function `{}` because it doesn't exist.", + instruction_name, + export_name, + )) + } + }) + } +); + +#[cfg(test)] +mod tests { + test_executable_instruction!( + test_call_export = + instructions: [ + Instruction::ArgumentGet { index: 1 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::CallExport { export_name: "sum" }, + ], + invocation_inputs: [ + InterfaceValue::I32(3), + InterfaceValue::I32(4), + ], + instance: Instance::new(), + stack: [InterfaceValue::I32(7)], + ); + + test_executable_instruction!( + test_call_export__invalid_export_name = + instructions: [Instruction::CallExport { export_name: "bar" }], + invocation_inputs: [], + instance: Instance::new(), + error: r#"`call-export "bar"` cannot call the exported function `bar` because it doesn't exist."#, + ); + + test_executable_instruction!( + test_call_export__stack_is_too_small = + instructions: [ + Instruction::ArgumentGet { index: 0 }, + Instruction::CallExport { export_name: "sum" }, + ], + invocation_inputs: [ + InterfaceValue::I32(3), + InterfaceValue::I32(4), + ], + instance: Instance::new(), + error: r#"`call-export "sum"` cannot call the exported function `sum` because there is not enough data on the stack for the arguments (needs 2)."#, + ); + + test_executable_instruction!( + test_call_export__invalid_types_in_the_stack = + instructions: [ + Instruction::ArgumentGet { index: 1 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::CallExport { export_name: "sum" }, + ], + invocation_inputs: [ + InterfaceValue::I32(3), + InterfaceValue::I64(4), + // ^^^ mismatch with `sum` signature + ], + instance: Instance::new(), + error: r#"`call-export "sum"` cannot call the exported function `sum` because the value types on the stack mismatch the function signature (expects [I32, I32])."#, + ); + + test_executable_instruction!( + test_call_export__failure_when_calling = + instructions: [ + Instruction::ArgumentGet { index: 1 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::CallExport { export_name: "sum" }, + ], + invocation_inputs: [ + InterfaceValue::I32(3), + InterfaceValue::I32(4), + ], + instance: Instance { + exports: { + let mut hashmap = HashMap::new(); + hashmap.insert( + "sum".into(), + Export { + inputs: vec![InterfaceType::I32, InterfaceType::I32], + outputs: vec![InterfaceType::I32], + function: |_| Err(()), + // ^^^^^^^ function fails + }, + ); + + hashmap + }, + ..Default::default() + }, + error: r#"`call-export "sum"` failed when calling the exported function `sum`."#, + ); + + test_executable_instruction!( + test_call_export__void = + instructions: [ + Instruction::ArgumentGet { index: 1 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::CallExport { export_name: "sum" }, + ], + invocation_inputs: [ + InterfaceValue::I32(3), + InterfaceValue::I32(4), + ], + instance: Instance { + exports: { + let mut hashmap = HashMap::new(); + hashmap.insert( + "sum".into(), + Export { + inputs: vec![InterfaceType::I32, InterfaceType::I32], + outputs: vec![InterfaceType::I32], + function: |_| Ok(vec![]), + // ^^^^^^^^^^ void function + }, + ); + + hashmap + }, + ..Default::default() + }, + stack: [], + ); +} diff --git a/src/interpreter/instructions/mod.rs b/src/interpreter/instructions/mod.rs new file mode 100644 index 0000000..05c3f97 --- /dev/null +++ b/src/interpreter/instructions/mod.rs @@ -0,0 +1,177 @@ +mod argument_get; +mod call; +mod call_export; +mod read_utf8; +mod write_utf8; + +pub(crate) use argument_get::argument_get; +pub(crate) use call::call; +pub(crate) use call_export::call_export; +pub(crate) use read_utf8::read_utf8; +pub(crate) use write_utf8::write_utf8; + +#[cfg(test)] +pub(crate) mod tests { + use crate::interpreter::wasm::{ + self, + values::{InterfaceType, InterfaceValue, ValueType}, + }; + use std::{cell::Cell, collections::HashMap, convert::TryInto}; + + pub(crate) struct Export { + pub(crate) inputs: Vec, + pub(crate) outputs: Vec, + pub(crate) function: fn(arguments: &[InterfaceValue]) -> Result, ()>, + } + + impl wasm::structures::Export for Export { + fn inputs_cardinality(&self) -> usize { + self.inputs.len() as usize + } + + fn outputs_cardinality(&self) -> usize { + self.outputs.len() + } + + fn inputs(&self) -> &[InterfaceType] { + &self.inputs + } + + fn outputs(&self) -> &[InterfaceType] { + &self.outputs + } + + fn call(&self, arguments: &[InterfaceValue]) -> Result, ()> { + (self.function)(arguments) + } + } + + pub(crate) struct LocalImport { + pub(crate) inputs: Vec, + pub(crate) outputs: Vec, + pub(crate) function: fn(arguments: &[InterfaceValue]) -> Result, ()>, + } + + impl wasm::structures::LocalImport for LocalImport { + fn inputs_cardinality(&self) -> usize { + self.inputs.len() as usize + } + + fn outputs_cardinality(&self) -> usize { + self.outputs.len() + } + + fn inputs(&self) -> &[InterfaceType] { + &self.inputs + } + + fn outputs(&self) -> &[InterfaceType] { + &self.outputs + } + + fn call(&self, arguments: &[InterfaceValue]) -> Result, ()> { + (self.function)(arguments) + } + } + + #[derive(Default)] + pub(crate) struct Memory { + pub(crate) data: Vec>, + } + + impl Memory { + pub(crate) fn new(data: Vec>) -> Self { + Self { data } + } + } + + impl wasm::structures::Memory for Memory { + fn view(&self) -> &[Cell] { + use std::slice; + + let slice = self.data.as_slice(); + + unsafe { slice::from_raw_parts(slice.as_ptr() as *const Cell, slice.len()) } + } + } + + #[derive(Default)] + pub(crate) struct Instance { + pub(crate) exports: HashMap, + pub(crate) locals_or_imports: HashMap, + pub(crate) memory: Memory, + } + + impl Instance { + pub(crate) fn new() -> Self { + Self { + exports: { + let mut hashmap = HashMap::new(); + hashmap.insert( + "sum".into(), + Export { + inputs: vec![InterfaceType::I32, InterfaceType::I32], + outputs: vec![InterfaceType::I32], + function: |arguments: &[InterfaceValue]| { + let a: i32 = (&arguments[0]).try_into().unwrap(); + let b: i32 = (&arguments[1]).try_into().unwrap(); + + Ok(vec![InterfaceValue::I32(a + b)]) + }, + }, + ); + hashmap.insert( + "alloc".into(), + Export { + inputs: vec![InterfaceType::I32], + outputs: vec![InterfaceType::I32], + function: |arguments: &[InterfaceValue]| { + let _size: i32 = (&arguments[0]).try_into().unwrap(); + + Ok(vec![InterfaceValue::I32(0)]) + }, + }, + ); + + hashmap + }, + locals_or_imports: { + let mut hashmap = HashMap::new(); + hashmap.insert( + 42, + LocalImport { + inputs: vec![InterfaceType::I32, InterfaceType::I32], + outputs: vec![InterfaceType::I32], + function: |arguments: &[InterfaceValue]| { + let a: i32 = (&arguments[0]).try_into().unwrap(); + let b: i32 = (&arguments[1]).try_into().unwrap(); + + Ok(vec![InterfaceValue::I32(a * b)]) + }, + }, + ); + + hashmap + }, + memory: Memory::new(vec![Cell::new(0); 128]), + } + } + } + + impl wasm::structures::Instance for Instance { + fn export(&self, export_name: &str) -> Option<&Export> { + self.exports.get(export_name) + } + + fn local_or_import( + &self, + index: I, + ) -> Option<&LocalImport> { + self.locals_or_imports.get(&index.index()) + } + + fn memory(&self, _index: usize) -> Option<&Memory> { + Some(&self.memory) + } + } +} diff --git a/src/interpreter/instructions/read_utf8.rs b/src/interpreter/instructions/read_utf8.rs new file mode 100644 index 0000000..8f195fd --- /dev/null +++ b/src/interpreter/instructions/read_utf8.rs @@ -0,0 +1,131 @@ +use crate::interpreter::wasm::values::InterfaceValue; +use std::{cell::Cell, convert::TryFrom}; + +executable_instruction!( + read_utf8(instruction_name: String) -> _ { + Box::new(move |runtime| -> _ { + match runtime.stack.pop(2) { + Some(inputs) => match runtime.wasm_instance.memory(0) { + Some(memory) => { + let length = i32::try_from(&inputs[0])? as usize; + let pointer = i32::try_from(&inputs[1])? as usize; + let memory_view = memory.view::(); + + if memory_view.len() < pointer + length { + return Err(format!( + "`{}` failed because it has to read out of the memory bounds (index {} > memory length {}).", + instruction_name, + pointer + length, + memory_view.len() + )); + } + + let data: Vec = (&memory_view[pointer..pointer + length]) + .iter() + .map(Cell::get) + .collect(); + + match String::from_utf8(data) { + Ok(string) => { + runtime.stack.push(InterfaceValue::String(string)); + + Ok(()) + } + Err(utf8_error) => Err(format!( + "`{}` failed because the read string isn't UTF-8 valid ({}).", + instruction_name, + utf8_error, + )) + } + } + None => Err(format!( + "`{}` failed because there is no memory to read.", + instruction_name + )) + } + None => Err(format!( + "`{}` failed because there is not enough data on the stack (needs 2).", + instruction_name, + )) + } + }) + } +); + +#[cfg(test)] +mod tests { + test_executable_instruction!( + test_read_utf8 = + instructions: [ + Instruction::ArgumentGet { index: 1 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::ReadUtf8, + ], + invocation_inputs: [ + InterfaceValue::I32(13), + // ^^^^^^^ length + InterfaceValue::I32(0), + // ^^^^^^ pointer + ], + instance: Instance { + memory: Memory::new("Hello, World!".as_bytes().iter().map(|u| Cell::new(*u)).collect()), + ..Default::default() + }, + stack: [InterfaceValue::String("Hello, World!".into())], + ); + + test_executable_instruction!( + test_read_utf8__read_out_of_memory = + instructions: [ + Instruction::ArgumentGet { index: 1 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::ReadUtf8, + ], + invocation_inputs: [ + InterfaceValue::I32(13), + // ^^^^^^^ length is too long + InterfaceValue::I32(0), + // ^^^^^^ pointer + ], + instance: Instance { + memory: Memory::new("Hello!".as_bytes().iter().map(|u| Cell::new(*u)).collect()), + ..Default::default() + }, + error: r#"`read-utf8` failed because it has to read out of the memory bounds (index 13 > memory length 6)."#, + ); + + test_executable_instruction!( + test_read_utf8__invalid_encoding = + instructions: [ + Instruction::ArgumentGet { index: 1 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::ReadUtf8, + ], + invocation_inputs: [ + InterfaceValue::I32(4), + // ^^^^^^ length is too long + InterfaceValue::I32(0), + // ^^^^^^ pointer + ], + instance: Instance { + memory: Memory::new(vec![0, 159, 146, 150].iter().map(|b| Cell::new(*b)).collect::>>()), + ..Default::default() + }, + error: r#"`read-utf8` failed because the read string isn't UTF-8 valid (invalid utf-8 sequence of 1 bytes from index 1)."#, + ); + + test_executable_instruction!( + test_read_utf8__stack_is_too_small = + instructions: [ + Instruction::ArgumentGet { index: 0 }, + Instruction::ReadUtf8, + // ^^^^^^^^ `read-utf8` expects 2 values on the stack, only one is present. + ], + invocation_inputs: [ + InterfaceValue::I32(13), + InterfaceValue::I32(0), + ], + instance: Instance::new(), + error: r#"`read-utf8` failed because there is not enough data on the stack (needs 2)."#, + ); +} diff --git a/src/interpreter/instructions/write_utf8.rs b/src/interpreter/instructions/write_utf8.rs new file mode 100644 index 0000000..fa66621 --- /dev/null +++ b/src/interpreter/instructions/write_utf8.rs @@ -0,0 +1,169 @@ +use crate::interpreter::wasm::values::{InterfaceType, InterfaceValue}; +use std::convert::TryInto; + +executable_instruction!( + write_utf8(allocator_name: String, instruction_name: String) -> _ { + Box::new(move |runtime| -> _ { + let instance = runtime.wasm_instance; + + match instance.export(&allocator_name) { + Some(allocator) => { + if allocator.inputs() != [InterfaceType::I32] || + allocator.outputs() != [InterfaceType::I32] { + return Err(format!( + "`{}` failed because the allocator `{}` has an invalid signature (expects [I32] -> [I32]).", + instruction_name, + allocator_name, + )) + } + + match runtime.wasm_instance.memory(0) { + Some(memory) => match runtime.stack.pop1() { + Some(string) => { + let memory_view = memory.view::(); + + let string: String = (&string).try_into()?; + let string_bytes = string.as_bytes(); + let string_length = (string_bytes.len() as i32) + .try_into() + .map_err(|error| format!("{}", error))?; + + match allocator.call(&[InterfaceValue::I32(string_length)]) { + Ok(outputs) => { + let string_pointer: i32 = (&outputs[0]).try_into()?; + + for (nth, byte) in string_bytes.iter().enumerate() { + memory_view[string_pointer as usize + nth].set(*byte); + } + + runtime.stack.push(InterfaceValue::I32(string_pointer)); + runtime.stack.push(InterfaceValue::I32(string_length)); + + Ok(()) + } + Err(_) => Err(format!( + "`{}` failed when calling the allocator `{}`.", + instruction_name, + allocator_name, + )) + } + } + None => Err(format!( + "`{}` cannot call the allocator `{}` because there is not enough data on the stack for the arguments (needs {}).", + instruction_name, + allocator_name, + 1 + )) + } + None => Err(format!( + "`{}` failed because there is no memory to write into.", + instruction_name + )) + } + } + None => Err(format!( + "`{}` failed because the exported function `{}` (the allocator) doesn't exist.", + instruction_name, + allocator_name + )) + } + }) + } +); + +#[cfg(test)] +mod tests { + test_executable_instruction!( + test_write_utf8 = + instructions: [ + Instruction::ArgumentGet { index: 0 }, + Instruction::WriteUtf8 { allocator_name: "alloc" }, + ], + invocation_inputs: [InterfaceValue::String("Hello, World!".into())], + instance: Instance::new(), + stack: [ + InterfaceValue::I32(0), + // ^^^^^^ pointer + InterfaceValue::I32(13), + // ^^^^^^^ length + ] + ); + + test_executable_instruction!( + test_write_utf8__roundtrip_with_read_utf8 = + instructions: [ + Instruction::ArgumentGet { index: 0 }, + Instruction::WriteUtf8 { allocator_name: "alloc" }, + Instruction::ReadUtf8, + ], + invocation_inputs: [InterfaceValue::String("Hello, World!".into())], + instance: Instance::new(), + stack: [InterfaceValue::String("Hello, World!".into())], + ); + + test_executable_instruction!( + test_write_utf8__allocator_does_not_exist = + instructions: [Instruction::WriteUtf8 { allocator_name: "alloc" }], + invocation_inputs: [], + instance: Instance { ..Default::default() }, + error: r#"`write-utf8 "alloc"` failed because the exported function `alloc` (the allocator) doesn't exist."#, + ); + + test_executable_instruction!( + test_write_utf8__stack_is_too_small = + instructions: [ + Instruction::WriteUtf8 { allocator_name: "alloc" } + // ^^^^^ `alloc` expects 1 value on the stack, none is present + ], + invocation_inputs: [InterfaceValue::String("Hello, World!".into())], + instance: Instance::new(), + error: r#"`write-utf8 "alloc"` cannot call the allocator `alloc` because there is not enough data on the stack for the arguments (needs 1)."#, + ); + + test_executable_instruction!( + test_write_utf8__failure_when_calling_the_allocator = + instructions: [ + Instruction::ArgumentGet { index: 0 }, + Instruction::WriteUtf8 { allocator_name: "alloc-fail" } + ], + invocation_inputs: [InterfaceValue::String("Hello, World!".into())], + instance: { + let mut instance = Instance::new(); + instance.exports.insert( + "alloc-fail".into(), + Export { + inputs: vec![InterfaceType::I32], + outputs: vec![InterfaceType::I32], + function: |_| Err(()), + // ^^^^^^^ function fails + }, + ); + + instance + }, + error: r#"`write-utf8 "alloc-fail"` failed when calling the allocator `alloc-fail`."#, + ); + + test_executable_instruction!( + test_write_utf8__invalid_allocator_signature = + instructions: [ + Instruction::ArgumentGet { index: 0 }, + Instruction::WriteUtf8 { allocator_name: "alloc-fail" } + ], + invocation_inputs: [InterfaceValue::String("Hello, World!".into())], + instance: { + let mut instance = Instance::new(); + instance.exports.insert( + "alloc-fail".into(), + Export { + inputs: vec![InterfaceType::I32, InterfaceType::I32], + outputs: vec![], + function: |_| Err(()), + }, + ); + + instance + }, + error: r#"`write-utf8 "alloc-fail"` failed because the allocator `alloc-fail` has an invalid signature (expects [I32] -> [I32])."#, + ); +} diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs new file mode 100644 index 0000000..e8b45bd --- /dev/null +++ b/src/interpreter/mod.rs @@ -0,0 +1,143 @@ +mod instruction; +mod instructions; +pub mod stack; +pub mod wasm; + +pub use instruction::Instruction; +use stack::Stack; +use std::{convert::TryFrom, marker::PhantomData}; +use wasm::values::InterfaceValue; + +pub(crate) struct Runtime<'invocation, 'instance, Instance, Export, LocalImport, Memory> +where + Export: wasm::structures::Export + 'instance, + LocalImport: wasm::structures::LocalImport + 'instance, + Memory: wasm::structures::Memory + 'instance, + Instance: wasm::structures::Instance + 'instance, +{ + invocation_inputs: &'invocation [InterfaceValue], + stack: Stack, + wasm_instance: &'instance Instance, + _wasm_exports: PhantomData, + _wasm_locals_or_imports: PhantomData, + _wasm_memories: PhantomData, +} + +pub(crate) type ExecutableInstruction = + Box) -> Result<(), String>>; + +pub struct Interpreter +where + Export: wasm::structures::Export, + LocalImport: wasm::structures::LocalImport, + Memory: wasm::structures::Memory, + Instance: wasm::structures::Instance, +{ + executable_instructions: Vec>, +} + +impl Interpreter +where + Export: wasm::structures::Export, + LocalImport: wasm::structures::LocalImport, + Memory: wasm::structures::Memory, + Instance: wasm::structures::Instance, +{ + fn iter( + &self, + ) -> impl Iterator> + '_ + { + self.executable_instructions.iter() + } + + pub fn run( + &self, + invocation_inputs: &[InterfaceValue], + wasm_instance: &Instance, + ) -> Result, String> { + let mut runtime = Runtime { + invocation_inputs, + stack: Stack::new(), + wasm_instance, + _wasm_exports: PhantomData, + _wasm_locals_or_imports: PhantomData, + _wasm_memories: PhantomData, + }; + + for executable_instruction in self.iter() { + match executable_instruction(&mut runtime) { + Ok(_) => continue, + Err(message) => return Err(message), + } + } + + Ok(runtime.stack) + } +} + +impl<'binary_input, Instance, Export, LocalImport, Memory> TryFrom<&Vec>> + for Interpreter +where + Export: wasm::structures::Export, + LocalImport: wasm::structures::LocalImport, + Memory: wasm::structures::Memory, + Instance: wasm::structures::Instance, +{ + type Error = String; + + fn try_from(instructions: &Vec) -> Result { + let executable_instructions = instructions + .iter() + .map( + |instruction| -> ExecutableInstruction { + let instruction_representation: String = instruction.into(); + + match instruction { + Instruction::ArgumentGet { index } => { + instructions::argument_get(*index, instruction_representation) + } + Instruction::Call { function_index } => { + instructions::call(*function_index, instruction_representation) + } + Instruction::CallExport { export_name } => instructions::call_export( + (*export_name).to_owned(), + instruction_representation, + ), + Instruction::ReadUtf8 => { + instructions::read_utf8(instruction_representation) + } + Instruction::WriteUtf8 { allocator_name } => instructions::write_utf8( + (*allocator_name).to_owned(), + instruction_representation, + ), + _ => unimplemented!(), + } + }, + ) + .collect(); + + Ok(Interpreter { + executable_instructions, + }) + } +} + +#[cfg(test)] +mod tests { + use super::{Instruction, Interpreter}; + use std::convert::TryInto; + + #[test] + fn test_interpreter_from_instructions() { + let instructions = vec![ + Instruction::ArgumentGet { index: 0 }, + Instruction::ArgumentGet { index: 0 }, + Instruction::CallExport { export_name: "foo" }, + Instruction::ReadUtf8, + Instruction::Call { function_index: 7 }, + ]; + let interpreter: Interpreter<(), (), (), ()> = (&instructions).try_into().unwrap(); + + assert_eq!(interpreter.executable_instructions.len(), 5); + } +} diff --git a/src/instructions/stack.rs b/src/interpreter/stack.rs similarity index 100% rename from src/instructions/stack.rs rename to src/interpreter/stack.rs diff --git a/src/interpreter/wasm/mod.rs b/src/interpreter/wasm/mod.rs new file mode 100644 index 0000000..0ff75a9 --- /dev/null +++ b/src/interpreter/wasm/mod.rs @@ -0,0 +1,2 @@ +pub mod structures; +pub mod values; diff --git a/src/instructions/wasm.rs b/src/interpreter/wasm/structures.rs similarity index 58% rename from src/instructions/wasm.rs rename to src/interpreter/wasm/structures.rs index 014b95c..d1fa583 100644 --- a/src/instructions/wasm.rs +++ b/src/interpreter/wasm/structures.rs @@ -1,88 +1,5 @@ -use std::{cell::Cell, convert::TryFrom}; - -pub use crate::ast::InterfaceType; - -#[derive(Debug, Clone, PartialEq)] -pub enum InterfaceValue { - Int(isize), - Float(f64), - Any(isize), - String(String), - // Seq(…), - I32(i32), - I64(i64), - F32(f32), - F64(f64), - // AnyRef(…), -} - -impl From<&InterfaceValue> for InterfaceType { - fn from(value: &InterfaceValue) -> Self { - match value { - InterfaceValue::Int(_) => Self::Int, - InterfaceValue::Float(_) => Self::Float, - InterfaceValue::Any(_) => Self::Any, - InterfaceValue::String(_) => Self::String, - InterfaceValue::I32(_) => Self::I32, - InterfaceValue::I64(_) => Self::I64, - InterfaceValue::F32(_) => Self::F32, - InterfaceValue::F64(_) => Self::F64, - } - } -} - -impl Default for InterfaceValue { - fn default() -> Self { - Self::I32(0) - } -} - -macro_rules! from_x_for_interface_value { - ($native_type:ty, $value_variant:ident) => { - impl From<$native_type> for InterfaceValue { - fn from(n: $native_type) -> Self { - Self::$value_variant(n) - } - } - - impl TryFrom<&InterfaceValue> for $native_type { - type Error = &'static str; - - fn try_from(w: &InterfaceValue) -> Result { - match w { - InterfaceValue::$value_variant(n) => Ok(n.clone()), - _ => Err("Invalid cast."), - } - } - } - }; -} - -from_x_for_interface_value!(String, String); -from_x_for_interface_value!(i32, I32); -from_x_for_interface_value!(i64, I64); -from_x_for_interface_value!(f32, F32); -from_x_for_interface_value!(f64, F64); - -pub trait ValueType: Copy -where - Self: Sized, -{ -} - -macro_rules! value_type { - ($native_type:ty) => { - impl ValueType for $native_type {} - }; - - ($($native_type:ty),*) => { - $( - value_type!($native_type); - )* - }; -} - -value_type!(u8, i8, u16, i16, u32, i32, u64, i64, f32, f64); +use super::values::{InterfaceType, InterfaceValue, ValueType}; +use std::cell::Cell; pub trait TypedIndex: Copy + Clone { fn new(index: usize) -> Self; diff --git a/src/interpreter/wasm/values.rs b/src/interpreter/wasm/values.rs new file mode 100644 index 0000000..1cfac91 --- /dev/null +++ b/src/interpreter/wasm/values.rs @@ -0,0 +1,85 @@ +use std::convert::TryFrom; + +pub use crate::ast::InterfaceType; + +#[derive(Debug, Clone, PartialEq)] +pub enum InterfaceValue { + Int(isize), + Float(f64), + Any(isize), + String(String), + // Seq(…), + I32(i32), + I64(i64), + F32(f32), + F64(f64), + // AnyRef(…), +} + +impl From<&InterfaceValue> for InterfaceType { + fn from(value: &InterfaceValue) -> Self { + match value { + InterfaceValue::Int(_) => Self::Int, + InterfaceValue::Float(_) => Self::Float, + InterfaceValue::Any(_) => Self::Any, + InterfaceValue::String(_) => Self::String, + InterfaceValue::I32(_) => Self::I32, + InterfaceValue::I64(_) => Self::I64, + InterfaceValue::F32(_) => Self::F32, + InterfaceValue::F64(_) => Self::F64, + } + } +} + +impl Default for InterfaceValue { + fn default() -> Self { + Self::I32(0) + } +} + +macro_rules! from_x_for_interface_value { + ($native_type:ty, $value_variant:ident) => { + impl From<$native_type> for InterfaceValue { + fn from(n: $native_type) -> Self { + Self::$value_variant(n) + } + } + + impl TryFrom<&InterfaceValue> for $native_type { + type Error = &'static str; + + fn try_from(w: &InterfaceValue) -> Result { + match w { + InterfaceValue::$value_variant(n) => Ok(n.clone()), + _ => Err("Invalid cast."), + } + } + } + }; +} + +from_x_for_interface_value!(String, String); +from_x_for_interface_value!(i32, I32); +from_x_for_interface_value!(i64, I64); +from_x_for_interface_value!(f32, F32); +from_x_for_interface_value!(f64, F64); + +pub trait ValueType: Copy +where + Self: Sized, +{ +} + +macro_rules! value_type { + ($native_type:ty) => { + impl ValueType for $native_type {} + }; + + ($($native_type:ty),*) => { + $( + value_type!($native_type); + )* + }; +} + +value_type!(u8, i8, u16, i16, u32, i32, u64, i64, f32, f64); diff --git a/src/lib.rs b/src/lib.rs index 748575c..2185b2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,13 +3,13 @@ pub mod ast; mod macros; pub mod decoders; pub mod encoders; -pub mod instructions; +pub mod interpreter; pub use decoders::binary::parse as parse_binary; #[cfg(test)] mod tests { - use crate::{ast::*, instructions::Instruction, parse_binary}; + use crate::{ast::*, interpreter::Instruction, parse_binary}; use std::fs; use wasmer_clif_backend::CraneliftCompiler; use wasmer_runtime_core as runtime; diff --git a/src/macros.rs b/src/macros.rs index 7dfc839..e6c0abf 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -23,3 +23,92 @@ macro_rules! consume { $input = next_input; }; } + +macro_rules! executable_instruction { + ($name:ident ( $($argument_name:ident: $argument_type:ty),* ) -> _ $implementation:block ) => { + use crate::interpreter::{ExecutableInstruction, wasm, stack::Stackable}; + + pub(crate) fn $name( + $($argument_name: $argument_type),* + ) -> ExecutableInstruction + where + Export: wasm::structures::Export, + LocalImport: wasm::structures::LocalImport, + Memory: wasm::structures::Memory, + Instance: wasm::structures::Instance, + { + $implementation + } + }; +} + +#[cfg(test)] +macro_rules! test_executable_instruction { + ( + $test_name:ident = + instructions: [ $($instructions:expr),* $(,)* ], + invocation_inputs: [ $($invocation_inputs:expr),* $(,)* ], + instance: $instance:expr, + stack: [ $($stack:expr),* $(,)* ] + $(,)* + ) => { + #[test] + #[allow(non_snake_case, unused)] + fn $test_name() { + use crate::interpreter::{ + instructions::tests::{Export, Instance, LocalImport, Memory}, + stack::Stackable, + wasm::values::{InterfaceType, InterfaceValue}, + Instruction, Interpreter, + }; + use std::{cell::Cell, collections::HashMap, convert::TryInto}; + + let interpreter: Interpreter = + (&vec![$($instructions),*]).try_into().unwrap(); + + let invocation_inputs = vec![$($invocation_inputs),*]; + let instance = $instance; + let run = interpreter.run(&invocation_inputs, &instance); + + assert!(run.is_ok()); + + let stack = run.unwrap(); + + assert_eq!(stack.as_slice(), &[$($stack),*]); + } + }; + + ( + $test_name:ident = + instructions: [ $($instructions:expr),* $(,)* ], + invocation_inputs: [ $($invocation_inputs:expr),* $(,)* ], + instance: $instance:expr, + error: $error:expr + $(,)* + ) => { + #[test] + #[allow(non_snake_case, unused)] + fn $test_name() { + use crate::interpreter::{ + instructions::tests::{Export, Instance, LocalImport, Memory}, + stack::Stackable, + wasm::values::{InterfaceType, InterfaceValue}, + Instruction, Interpreter, + }; + use std::{cell::Cell, collections::HashMap, convert::TryInto}; + + let interpreter: Interpreter = + (&vec![$($instructions),*]).try_into().unwrap(); + + let invocation_inputs = vec![$($invocation_inputs),*]; + let instance = $instance; + let run = interpreter.run(&invocation_inputs, &instance); + + assert!(run.is_err()); + + let error = run.unwrap_err(); + + assert_eq!(error, String::from($error)); + } + }; +}