Improve error handling, and start refactoring Instance.

Introduce proper error handling in several places, and perform a first
pass at refactoring Instance to make it easier to use.
This commit is contained in:
Dan Gohman
2018-12-07 15:32:51 -05:00
parent fe562297a7
commit 7dcca6be5b
24 changed files with 949 additions and 565 deletions

View File

@@ -24,6 +24,11 @@
extern crate cranelift_codegen;
extern crate cranelift_wasm;
#[macro_use]
extern crate cranelift_entity;
extern crate failure;
#[macro_use]
extern crate failure_derive;
extern crate target_lexicon;
extern crate wabt;
extern crate wasmtime_environ;
@@ -32,4 +37,4 @@ extern crate wasmtime_execute;
mod spectest;
mod wast;
pub use wast::{wast_buffer, wast_file};
pub use wast::{WastContext, WastError};

View File

@@ -53,17 +53,17 @@ impl SpecTest {
Self {
spectest_global_i32: VMGlobal::definition(&Global {
ty: types::I32,
mutability: false,
mutability: true,
initializer: GlobalInit::I32Const(0),
}),
spectest_global_f32: VMGlobal::definition(&Global {
ty: types::I32,
mutability: false,
mutability: true,
initializer: GlobalInit::F32Const(0),
}),
spectest_global_f64: VMGlobal::definition(&Global {
ty: types::I32,
mutability: false,
mutability: true,
initializer: GlobalInit::F64Const(0),
}),
spectest_table: VMTable::definition(ptr::null_mut(), 0),
@@ -79,7 +79,7 @@ impl Resolver for SpecTest {
match module {
"spectest" => match field {
"print" => Some(ExportValue::function(
spectest_print as usize,
spectest_print as *const u8,
translate_signature(
ir::Signature {
params: vec![],
@@ -90,7 +90,7 @@ impl Resolver for SpecTest {
),
)),
"print_i32" => Some(ExportValue::function(
spectest_print_i32 as usize,
spectest_print_i32 as *const u8,
translate_signature(
ir::Signature {
params: vec![ir::AbiParam::new(types::I32)],
@@ -101,7 +101,7 @@ impl Resolver for SpecTest {
),
)),
"print_i64" => Some(ExportValue::function(
spectest_print_i64 as usize,
spectest_print_i64 as *const u8,
translate_signature(
ir::Signature {
params: vec![ir::AbiParam::new(types::I64)],
@@ -112,7 +112,7 @@ impl Resolver for SpecTest {
),
)),
"print_f32" => Some(ExportValue::function(
spectest_print_f32 as usize,
spectest_print_f32 as *const u8,
translate_signature(
ir::Signature {
params: vec![ir::AbiParam::new(types::F32)],
@@ -123,7 +123,7 @@ impl Resolver for SpecTest {
),
)),
"print_f64" => Some(ExportValue::function(
spectest_print_f64 as usize,
spectest_print_f64 as *const u8,
translate_signature(
ir::Signature {
params: vec![ir::AbiParam::new(types::F64)],
@@ -134,7 +134,7 @@ impl Resolver for SpecTest {
),
)),
"print_i32_f32" => Some(ExportValue::function(
spectest_print_i32_f32 as usize,
spectest_print_i32_f32 as *const u8,
translate_signature(
ir::Signature {
params: vec![
@@ -148,7 +148,7 @@ impl Resolver for SpecTest {
),
)),
"print_f64_f64" => Some(ExportValue::function(
spectest_print_f64_f64 as usize,
spectest_print_f64_f64 as *const u8,
translate_signature(
ir::Signature {
params: vec![

View File

@@ -1,24 +1,98 @@
use cranelift_codegen::isa;
use cranelift_entity::PrimaryMap;
use spectest::SpecTest;
use std::collections::HashMap;
use std::fs;
use std::io;
use std::io::Read;
use std::path::Path;
use std::str;
use wabt::script::{self, Action, Command, CommandKind, ModuleBinary, ScriptParser};
use wasmtime_execute::{ActionOutcome, Code, InstanceWorld, Value};
use std::{fmt, fs, io, str};
use wabt::script::{Action, Command, CommandKind, ModuleBinary, ScriptParser, Value};
use wasmtime_execute::{ActionError, ActionOutcome, Code, InstanceWorld, RuntimeValue};
struct Instances {
current: Option<InstanceWorld>,
namespace: HashMap<String, InstanceWorld>,
/// Translate from a script::Value to a RuntimeValue.
fn runtime_value(v: Value) -> RuntimeValue {
match v {
Value::I32(x) => RuntimeValue::I32(x),
Value::I64(x) => RuntimeValue::I64(x),
Value::F32(x) => RuntimeValue::F32(x.to_bits()),
Value::F64(x) => RuntimeValue::F64(x.to_bits()),
}
}
/// Indicates an unknown module was specified.
#[derive(Fail, Debug)]
pub struct UnknownModule {
module: Option<String>,
}
impl fmt::Display for UnknownModule {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.module {
None => write!(f, "no default module present"),
Some(ref name) => write!(f, "no module {} present", name),
}
}
}
/// Error message used by `WastContext`.
#[derive(Fail, Debug)]
pub enum WastError {
/// An assert command was not satisfied.
Assert(String),
/// An unknown module name was used.
Module(UnknownModule),
/// An error occured while performing an action.
Action(ActionError),
/// An action trapped.
Trap(String),
/// There was a type error in inputs or outputs of an action.
Type(String),
/// The was an I/O error while reading the wast file.
IO(io::Error),
}
impl fmt::Display for WastError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
WastError::Assert(ref message) => write!(f, "Assert command failed: {}", message),
WastError::Module(ref error) => error.fmt(f),
WastError::Action(ref error) => error.fmt(f),
WastError::Trap(ref message) => write!(f, "trap: {}", message),
WastError::Type(ref message) => write!(f, "type error: {}", message),
WastError::IO(ref error) => write!(f, "I/O error: {}", error),
}
}
}
/// Error message with a source file and line number.
#[derive(Fail, Debug)]
#[fail(display = "{}:{}: {}", filename, line, error)]
pub struct WastFileError {
filename: String,
line: u64,
error: WastError,
}
/// An opaque reference to an `InstanceWorld`.
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct WorldIndex(u32);
entity_impl!(WorldIndex, "world");
/// The wast test script language allows modules to be defined and actions
/// to be performed on them.
pub struct WastContext {
/// A namespace of wasm modules, keyed by an optional name.
worlds: PrimaryMap<WorldIndex, InstanceWorld>,
current: Option<WorldIndex>,
namespace: HashMap<String, WorldIndex>,
code: Code,
spectest: SpecTest,
}
impl Instances {
impl WastContext {
/// Construct a new instance of `WastContext`.
pub fn new() -> Self {
Self {
worlds: PrimaryMap::new(),
current: None,
namespace: HashMap::new(),
code: Code::new(),
@@ -30,290 +104,380 @@ impl Instances {
&mut self,
isa: &isa::TargetIsa,
module: ModuleBinary,
) -> Result<InstanceWorld, String> {
) -> Result<InstanceWorld, ActionError> {
InstanceWorld::new(&mut self.code, isa, &module.into_vec(), &mut self.spectest)
}
pub fn define_unnamed_module(
&mut self,
isa: &isa::TargetIsa,
module: ModuleBinary,
) -> Result<(), String> {
self.current = Some(self.instantiate(isa, module)?);
Ok(())
fn get_world(&mut self, module: &Option<String>) -> Result<WorldIndex, WastError> {
let index = *if let Some(name) = module {
self.namespace.get_mut(name).ok_or_else(|| {
WastError::Module(UnknownModule {
module: Some(name.to_owned()),
})
})
} else {
self.current
.as_mut()
.ok_or_else(|| WastError::Module(UnknownModule { module: None }))
}?;
Ok(index)
}
pub fn define_named_module(
/// Define a module and register it.
pub fn module(
&mut self,
isa: &isa::TargetIsa,
name: String,
name: Option<String>,
module: ModuleBinary,
) -> Result<(), String> {
) -> Result<(), ActionError> {
let world = self.instantiate(isa, module)?;
self.namespace.insert(name, world);
let index = if let Some(name) = name {
self.register(name, world)
} else {
self.worlds.push(world)
};
self.current = Some(index);
Ok(())
}
pub fn perform_action(
/// Register a module to make it available for performing actions.
pub fn register(&mut self, name: String, world: InstanceWorld) -> WorldIndex {
let index = self.worlds.push(world);
self.namespace.insert(name, index);
index
}
/// Invoke an exported function from a defined module.
pub fn invoke(
&mut self,
isa: &isa::TargetIsa,
module: Option<String>,
field: &str,
args: &[Value],
) -> Result<ActionOutcome, WastError> {
let mut value_args = Vec::with_capacity(args.len());
for arg in args {
value_args.push(runtime_value(*arg));
}
let index = self.get_world(&module)?;
self.worlds[index]
.invoke(&mut self.code, isa, &field, &value_args)
.map_err(WastError::Action)
}
/// Get the value of an exported global from a defined module.
pub fn get(&mut self, module: Option<String>, field: &str) -> Result<RuntimeValue, WastError> {
let index = self.get_world(&module)?;
self.worlds[index].get(&field).map_err(WastError::Action)
}
fn perform_action(
&mut self,
isa: &isa::TargetIsa,
action: Action,
) -> Result<ActionOutcome, String> {
) -> Result<ActionOutcome, WastError> {
match action {
Action::Invoke {
module,
field,
args,
} => {
let mut value_args = Vec::with_capacity(args.len());
for a in args {
value_args.push(match a {
script::Value::I32(i) => Value::I32(i),
script::Value::I64(i) => Value::I64(i),
script::Value::F32(i) => Value::F32(i.to_bits()),
script::Value::F64(i) => Value::F64(i.to_bits()),
});
}
match module {
None => match self.current {
None => Err("invoke performed with no module present".to_string()),
Some(ref mut instance_world) => instance_world
.invoke(&mut self.code, isa, &field, &value_args)
.map_err(|e| {
format!("error invoking {} in current module: {}", field, e)
}),
},
Some(name) => self
.namespace
.get_mut(&name)
.ok_or_else(|| format!("module {} not declared", name))?
.invoke(&mut self.code, isa, &field, &value_args)
.map_err(|e| format!("error invoking {} in module {}: {}", field, name, e)),
}
}
} => self.invoke(isa, module, &field, &args),
Action::Get { module, field } => {
let value = match module {
None => match self.current {
None => return Err("get performed with no module present".to_string()),
Some(ref mut instance_world) => {
instance_world.get(&field).map_err(|e| {
format!("error getting {} in current module: {}", field, e)
})?
}
},
Some(name) => self
.namespace
.get_mut(&name)
.ok_or_else(|| format!("module {} not declared", name))?
.get(&field)
.map_err(|e| {
format!("error getting {} in module {}: {}", field, name, e)
})?,
};
let value = self.get(module, &field)?;
Ok(ActionOutcome::Returned {
values: vec![value],
})
}
}
}
}
/// Run a wast script from a byte buffer.
pub fn wast_buffer(name: &str, isa: &isa::TargetIsa, wast: &[u8]) -> Result<(), String> {
let mut parser = ScriptParser::from_str(str::from_utf8(wast).unwrap()).unwrap();
let mut instances = Instances::new();
/// Run a wast script from a byte buffer.
pub fn run_buffer(
&mut self,
isa: &isa::TargetIsa,
filename: &str,
wast: &[u8],
) -> Result<(), WastFileError> {
let mut parser = ScriptParser::from_str(str::from_utf8(wast).unwrap()).unwrap();
while let Some(Command { kind, line }) = parser.next().unwrap() {
match kind {
CommandKind::Module { module, name } => {
if let Some(name) = name {
instances.define_named_module(isa, name, module.clone())?;
while let Some(Command { kind, line }) = parser.next().unwrap() {
match kind {
CommandKind::Module { module, name } => {
self.module(isa, name, module)
.map_err(|error| WastFileError {
filename: filename.to_string(),
line,
error: WastError::Action(error),
})?;
}
instances.define_unnamed_module(isa, module)?;
}
CommandKind::Register {
name: _name,
as_name: _as_name,
} => {
println!("{}:{}: TODO: Implement register", name, line);
}
CommandKind::PerformAction(action) => match instances.perform_action(isa, action)? {
ActionOutcome::Returned { .. } => {}
ActionOutcome::Trapped { message } => {
panic!("{}:{}: a trap occurred: {}", name, line, message);
CommandKind::Register {
name: _name,
as_name: _as_name,
} => {
println!("{}:{}: TODO: Implement register", filename, line);
}
},
CommandKind::AssertReturn { action, expected } => {
match instances.perform_action(isa, action)? {
ActionOutcome::Returned { values } => {
for (v, e) in values.iter().zip(expected.iter()) {
match *e {
script::Value::I32(x) => {
assert_eq!(x, v.unwrap_i32(), "at {}:{}", name, line)
CommandKind::PerformAction(action) => match self
.perform_action(isa, action)
.map_err(|error| WastFileError {
filename: filename.to_string(),
line,
error,
})? {
ActionOutcome::Returned { .. } => {}
ActionOutcome::Trapped { message } => {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Trap(message),
});
}
},
CommandKind::AssertReturn { action, expected } => {
match self
.perform_action(isa, action)
.map_err(|error| WastFileError {
filename: filename.to_string(),
line,
error,
})? {
ActionOutcome::Returned { values } => {
for (v, e) in values
.iter()
.cloned()
.zip(expected.iter().cloned().map(runtime_value))
{
if v != e {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Assert(format!(
"expected {}, got {}",
e, v
)),
});
}
script::Value::I64(x) => {
assert_eq!(x, v.unwrap_i64(), "at {}:{}", name, line)
}
script::Value::F32(x) => {
assert_eq!(x.to_bits(), v.unwrap_f32(), "at {}:{}", name, line)
}
script::Value::F64(x) => {
assert_eq!(x.to_bits(), v.unwrap_f64(), "at {}:{}", name, line)
}
};
}
}
ActionOutcome::Trapped { message } => {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Assert(format!("unexpected trap: {}", message)),
});
}
}
ActionOutcome::Trapped { message } => {
panic!(
"{}:{}: expected normal return, but a trap occurred: {}",
name, line, message
);
}
}
}
CommandKind::AssertTrap { action, message } => {
match instances.perform_action(isa, action)? {
ActionOutcome::Returned { values } => panic!(
"{}:{}: expected trap, but invoke returned with {:?}",
name, line, values
),
ActionOutcome::Trapped {
message: trap_message,
} => {
println!(
"{}:{}: TODO: Check the assert_trap message: expected {}, got {}",
name, line, message, trap_message
);
}
}
}
CommandKind::AssertExhaustion { action } => {
match instances.perform_action(isa, action)? {
ActionOutcome::Returned { values } => panic!(
"{}:{}: expected exhaustion, but invoke returned with {:?}",
name, line, values
),
ActionOutcome::Trapped { message } => {
println!(
"{}:{}: TODO: Check the assert_exhaustion message: {}",
name, line, message
);
}
}
}
CommandKind::AssertReturnCanonicalNan { action } => {
match instances.perform_action(isa, action)? {
ActionOutcome::Returned { values } => {
for v in values.iter() {
match v {
Value::I32(_) | Value::I64(_) => {
panic!("unexpected integer type in NaN test");
}
Value::F32(x) => assert_eq!(
x & 0x7fffffff,
0x7fc00000,
"expected canonical NaN at {}:{}",
name,
line
),
Value::F64(x) => assert_eq!(
x & 0x7fffffffffffffff,
0x7ff8000000000000,
"expected canonical NaN at {}:{}",
name,
line
),
};
CommandKind::AssertTrap { action, message } => {
match self
.perform_action(isa, action)
.map_err(|error| WastFileError {
filename: filename.to_string(),
line,
error,
})? {
ActionOutcome::Returned { values } => {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Assert(format!(
"expected trap, but invoke returned with {:?}",
values
)),
});
}
ActionOutcome::Trapped {
message: trap_message,
} => {
println!(
"{}:{}: TODO: Check the assert_trap message: expected {}, got {}",
filename, line, message, trap_message
);
}
}
ActionOutcome::Trapped { message } => {
panic!(
"{}:{}: expected canonical NaN return, but a trap occurred: {}",
name, line, message
);
}
}
}
CommandKind::AssertReturnArithmeticNan { action } => {
match instances.perform_action(isa, action)? {
ActionOutcome::Returned { values } => {
for v in values.iter() {
match v {
Value::I32(_) | Value::I64(_) => {
panic!("unexpected integer type in NaN test");
}
Value::F32(x) => assert_eq!(
x & 0x00400000,
0x00400000,
"expected arithmetic NaN at {}:{}",
name,
line
),
Value::F64(x) => assert_eq!(
x & 0x0008000000000000,
0x0008000000000000,
"expected arithmetic NaN at {}:{}",
name,
line
),
};
CommandKind::AssertExhaustion { action } => {
match self
.perform_action(isa, action)
.map_err(|error| WastFileError {
filename: filename.to_string(),
line,
error,
})? {
ActionOutcome::Returned { values } => {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Assert(format!(
"expected callstack exhaustion, but invoke returned with {:?}",
values
)),
});
}
ActionOutcome::Trapped { message } => {
println!(
"{}:{}: TODO: Check the assert_exhaustion message: {}",
filename, line, message
);
}
}
ActionOutcome::Trapped { message } => {
panic!(
"{}:{}: expected canonical NaN return, but a trap occurred: {}",
name, line, message
);
}
CommandKind::AssertReturnCanonicalNan { action } => {
match self
.perform_action(isa, action)
.map_err(|error| WastFileError {
filename: filename.to_string(),
line,
error,
})? {
ActionOutcome::Returned { values } => {
for v in values.iter() {
match v {
RuntimeValue::I32(_) | RuntimeValue::I64(_) => {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Type(format!(
"unexpected integer type in NaN test"
)),
});
}
RuntimeValue::F32(x) => {
if (x & 0x7fffffff) != 0x7fc00000 {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Assert(format!(
"expected canonical NaN"
)),
});
}
}
RuntimeValue::F64(x) => {
if (x & 0x7fffffffffffffff) != 0x7ff8000000000000 {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Assert(format!(
"expected canonical NaN"
)),
});
}
}
};
}
}
ActionOutcome::Trapped { message } => {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Assert(format!("unexpected trap: {}", message)),
});
}
}
}
}
CommandKind::AssertInvalid {
module: _module,
message: _message,
} => {
println!("{}:{}: TODO: Implement assert_invalid", name, line);
}
CommandKind::AssertMalformed {
module: _module,
message: _message,
} => {
println!("{}:{}: TODO: Implement assert_malformed", name, line);
}
CommandKind::AssertUninstantiable { module, message } => {
let _err = instances
.define_unnamed_module(isa, module)
.expect_err(&format!(
CommandKind::AssertReturnArithmeticNan { action } => {
match self
.perform_action(isa, action)
.map_err(|error| WastFileError {
filename: filename.to_string(),
line,
error,
})? {
ActionOutcome::Returned { values } => {
for v in values.iter() {
match v {
RuntimeValue::I32(_) | RuntimeValue::I64(_) => {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Type(format!(
"unexpected integer type in NaN test",
)),
});
}
RuntimeValue::F32(x) => {
if (x & 0x00400000) != 0x00400000 {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Assert(format!(
"expected arithmetic NaN"
)),
});
}
}
RuntimeValue::F64(x) => {
if (x & 0x0008000000000000) != 0x0008000000000000 {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Assert(format!(
"expected arithmetic NaN"
)),
});
}
}
};
}
}
ActionOutcome::Trapped { message } => {
return Err(WastFileError {
filename: filename.to_string(),
line,
error: WastError::Assert(format!("unexpected trap: {}", message)),
});
}
}
}
CommandKind::AssertInvalid {
module: _module,
message: _message,
} => {
println!("{}:{}: TODO: Implement assert_invalid", filename, line);
}
CommandKind::AssertMalformed {
module: _module,
message: _message,
} => {
println!("{}:{}: TODO: Implement assert_malformed", filename, line);
}
CommandKind::AssertUninstantiable { module, message } => {
let _err = self.module(isa, None, module).expect_err(&format!(
"{}:{}: uninstantiable module was successfully instantiated",
name, line
filename, line
));
println!(
"{}:{}: TODO: Check the assert_uninstantiable message: {}",
name, line, message
);
}
CommandKind::AssertUnlinkable { module, message } => {
let _err = instances
.define_unnamed_module(isa, module)
.expect_err(&format!(
println!(
"{}:{}: TODO: Check the assert_uninstantiable message: {}",
filename, line, message
);
}
CommandKind::AssertUnlinkable { module, message } => {
let _err = self.module(isa, None, module).expect_err(&format!(
"{}:{}: unlinkable module was successfully linked",
name, line
filename, line
));
println!(
"{}:{}: TODO: Check the assert_unlinkable message: {}",
name, line, message
);
println!(
"{}:{}: TODO: Check the assert_unlinkable message: {}",
filename, line, message
);
}
}
}
Ok(())
}
Ok(())
}
/// Run a wast script from a file.
pub fn wast_file(path: &Path, isa: &isa::TargetIsa) -> Result<(), String> {
let wast = read_to_end(path).map_err(|e| e.to_string())?;
wast_buffer(&path.display().to_string(), isa, &wast)
/// Run a wast script from a file.
pub fn run_file(&mut self, isa: &isa::TargetIsa, path: &Path) -> Result<(), WastFileError> {
let filename = path.display().to_string();
let buffer = read_to_end(path).map_err(|e| WastFileError {
filename,
line: 0,
error: WastError::IO(e),
})?;
self.run_buffer(isa, &path.display().to_string(), &buffer)
}
}
fn read_to_end(path: &Path) -> Result<Vec<u8>, io::Error> {