use fizzyx::{
Config, Engine, Error, ExternKind, FuncType, Linker, Module, Mutability, Val, ValType,
};
fn wasm(wat: &str) -> Vec<u8> {
wat::parse_str(wat).expect("failed to assemble WAT")
}
fn instantiate(wat: &str) -> (Engine, fizzyx::Instance) {
let engine = Engine::default();
let module = Module::new(&wasm(wat)).expect("failed to parse module");
let linker = Linker::new(&engine);
let instance = linker.instantiate(&module).expect("failed to instantiate");
(engine, instance)
}
#[test]
fn validate_accepts_and_rejects() {
assert!(Module::validate(&wasm("(module)")).is_ok());
assert!(matches!(
Module::validate(&[0x00, 0x61, 0x73]),
Err(Error::Invalid(_))
));
}
#[test]
fn parse_rejects_garbage() {
assert!(matches!(
Module::new(&[0xde, 0xad, 0xbe, 0xef]),
Err(Error::Malformed(_))
));
}
#[test]
fn call_across_all_numeric_types() {
let (_engine, mut instance) = instantiate(
r#"(module
(func (export "add_i32") (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add)
(func (export "add_i64") (param i64 i64) (result i64)
local.get 0 local.get 1 i64.add)
(func (export "add_f32") (param f32 f32) (result f32)
local.get 0 local.get 1 f32.add)
(func (export "add_f64") (param f64 f64) (result f64)
local.get 0 local.get 1 f64.add))"#,
);
let i32_add = instance.get_func("add_i32").unwrap();
let mut out = [Val::I32(0)];
i32_add
.call(&mut instance, &[Val::I32(40), Val::I32(2)], &mut out)
.unwrap();
assert_eq!(out[0], Val::I32(42));
let i64_add = instance.get_func("add_i64").unwrap();
let mut out = [Val::I64(0)];
i64_add
.call(&mut instance, &[Val::I64(1), Val::I64(2)], &mut out)
.unwrap();
assert_eq!(out[0], Val::I64(3));
let f32_add = instance.get_func("add_f32").unwrap();
let mut out = [Val::F32(0.0)];
f32_add
.call(&mut instance, &[Val::F32(1.5), Val::F32(2.25)], &mut out)
.unwrap();
assert_eq!(out[0], Val::F32(3.75));
let f64_add = instance.get_func("add_f64").unwrap();
let mut out = [Val::F64(0.0)];
f64_add
.call(&mut instance, &[Val::F64(1.5), Val::F64(2.25)], &mut out)
.unwrap();
assert_eq!(out[0], Val::F64(3.75));
}
#[test]
fn func_with_no_result() {
let (_engine, mut instance) = instantiate(
r#"(module (global $g (mut i32) (i32.const 0))
(func (export "set") (param i32) global.get 0 drop local.get 0 global.set 0)
(global (export "g") (mut i32) (i32.const 0)))"#,
);
let set = instance.get_func("set").unwrap();
assert!(set.ty().results().is_empty());
set.call(&mut instance, &[Val::I32(7)], &mut []).unwrap();
}
#[test]
fn arity_and_type_mismatch_are_reported() {
let (_engine, mut instance) =
instantiate(r#"(module (func (export "id") (param i32) (result i32) local.get 0))"#);
let id = instance.get_func("id").unwrap();
let mut out = [Val::I32(0)];
assert!(matches!(
id.call(&mut instance, &[], &mut out),
Err(Error::ArityMismatch { .. })
));
assert!(matches!(
id.call(&mut instance, &[Val::I64(1)], &mut out),
Err(Error::TypeMismatch { .. })
));
}
#[test]
fn missing_export_returns_none() {
let (_engine, instance) = instantiate(r#"(module (func (export "a")))"#);
assert!(instance.get_func("does_not_exist").is_none());
assert!(instance.get_memory("mem").is_none());
assert!(instance.get_global("g").is_none());
}
#[test]
fn trap_is_reported() {
let (_engine, mut instance) =
instantiate(r#"(module (func (export "boom") (result i32) unreachable))"#);
let boom = instance.get_func("boom").unwrap();
let mut out = [Val::I32(0)];
assert!(matches!(
boom.call(&mut instance, &[], &mut out),
Err(Error::Trap)
));
let (_engine, mut instance) = instantiate(
r#"(module (func (export "div") (param i32 i32) (result i32)
local.get 0 local.get 1 i32.div_s))"#,
);
let div = instance.get_func("div").unwrap();
assert!(matches!(
div.call(&mut instance, &[Val::I32(1), Val::I32(0)], &mut out),
Err(Error::Trap)
));
}
#[test]
fn host_function_import() {
let engine = Engine::default();
let module = Module::new(&wasm(
r#"(module
(import "env" "mul" (func $mul (param i32 i32) (result i32)))
(func (export "run") (param i32 i32) (result i32)
local.get 0 local.get 1 call $mul))"#,
))
.unwrap();
let mut linker = Linker::new(&engine);
linker
.func_new(
"env",
"mul",
FuncType::new([ValType::I32, ValType::I32], [ValType::I32]),
|params, results| {
let a = params[0].i32().unwrap();
let b = params[1].i32().unwrap();
results[0] = Val::I32(a * b);
},
)
.unwrap();
let mut instance = linker.instantiate(&module).unwrap();
let run = instance.get_func("run").unwrap();
let mut out = [Val::I32(0)];
run.call(&mut instance, &[Val::I32(6), Val::I32(7)], &mut out)
.unwrap();
assert_eq!(out[0], Val::I32(42));
}
#[test]
fn host_function_panic_becomes_trap() {
let engine = Engine::default();
let module = Module::new(&wasm(
r#"(module
(import "env" "boom" (func $boom (result i32)))
(func (export "run") (result i32) call $boom))"#,
))
.unwrap();
let mut linker = Linker::new(&engine);
linker
.func_new(
"env",
"boom",
FuncType::new([], [ValType::I32]),
|_params, _results| panic!("host function exploded"),
)
.unwrap();
let mut instance = linker.instantiate(&module).unwrap();
let run = instance.get_func("run").unwrap();
let mut out = [Val::I32(0)];
assert!(matches!(
run.call(&mut instance, &[], &mut out),
Err(Error::Trap)
));
}
#[test]
fn unresolved_import_fails_instantiation() {
let engine = Engine::default();
let module = Module::new(&wasm(r#"(module (import "env" "missing" (func)))"#)).unwrap();
let linker = Linker::new(&engine);
assert!(matches!(
linker.instantiate(&module),
Err(Error::Instantiation(_))
));
}
#[test]
fn memory_read_write_roundtrip() {
let (_engine, mut instance) = instantiate(
r#"(module
(memory (export "memory") 1)
(func (export "load") (param i32) (result i32) local.get 0 i32.load))"#,
);
let memory = instance.get_memory("memory").unwrap();
assert_eq!(memory.size(&instance), 1);
assert_eq!(memory.data_size(&instance), 65536);
memory
.write(&mut instance, 16, &[0x2a, 0x00, 0x00, 0x00])
.unwrap();
let mut buf = [0u8; 4];
memory.read(&instance, 16, &mut buf).unwrap();
assert_eq!(buf, [0x2a, 0x00, 0x00, 0x00]);
let load = instance.get_func("load").unwrap();
let mut out = [Val::I32(0)];
load.call(&mut instance, &[Val::I32(16)], &mut out).unwrap();
assert_eq!(out[0], Val::I32(42));
}
#[test]
fn memory_out_of_bounds_is_reported() {
let (_engine, mut instance) = instantiate(r#"(module (memory (export "memory") 1))"#);
let memory = instance.get_memory("memory").unwrap();
let mut buf = [0u8; 8];
assert!(matches!(
memory.read(&instance, 65_534, &mut buf),
Err(Error::MemoryOutOfBounds { .. })
));
assert!(matches!(
memory.write(&mut instance, 65_534, &buf),
Err(Error::MemoryOutOfBounds { .. })
));
}
#[test]
fn global_get_and_set() {
let (_engine, mut instance) = instantiate(
r#"(module
(global (export "counter") (mut i32) (i32.const 10))
(global (export "answer") i32 (i32.const 42)))"#,
);
let counter = instance.get_global("counter").unwrap();
assert_eq!(counter.ty().content(), ValType::I32);
assert_eq!(counter.ty().mutability(), Mutability::Mutable);
assert_eq!(counter.get(&instance), Val::I32(10));
counter.set(&mut instance, Val::I32(11)).unwrap();
assert_eq!(counter.get(&instance), Val::I32(11));
let answer = instance.get_global("answer").unwrap();
assert_eq!(answer.get(&instance), Val::I32(42));
assert!(matches!(
answer.set(&mut instance, Val::I32(0)),
Err(Error::GlobalImmutable)
));
assert!(matches!(
counter.set(&mut instance, Val::I64(0)),
Err(Error::TypeMismatch { .. })
));
}
#[test]
fn module_introspection() {
let module = Module::new(&wasm(
r#"(module
(import "env" "log" (func (param i32)))
(import "env" "buf" (global i32))
(func (export "run"))
(memory (export "mem") 1)
(global (export "g") (mut i64) (i64.const 0)))"#,
))
.unwrap();
let imports = module.imports();
assert_eq!(imports.len(), 2);
assert!(imports.iter().any(|i| i.module() == "env"
&& i.name() == "log"
&& matches!(i.ty(), fizzyx::ExternType::Func(_))));
assert!(
imports
.iter()
.any(|i| i.name() == "buf" && matches!(i.ty(), fizzyx::ExternType::Global(_)))
);
let exports = module.exports();
assert!(
exports
.iter()
.any(|e| e.name() == "run" && e.kind() == ExternKind::Func)
);
assert!(
exports
.iter()
.any(|e| e.name() == "mem" && e.kind() == ExternKind::Memory)
);
assert!(
exports
.iter()
.any(|e| e.name() == "g" && e.kind() == ExternKind::Global)
);
assert!(module.func_type("run").is_some());
assert!(module.func_type("missing").is_none());
}
#[test]
fn module_is_reusable_across_instances() {
let engine = Engine::default();
let module = Module::new(&wasm(
r#"(module (func (export "answer") (result i32) i32.const 42))"#,
))
.unwrap();
let linker = Linker::new(&engine);
for _ in 0..3 {
let mut instance = linker.instantiate(&module).unwrap();
let f = instance.get_func("answer").unwrap();
let mut out = [Val::I32(0)];
f.call(&mut instance, &[], &mut out).unwrap();
assert_eq!(out[0], Val::I32(42));
}
}
#[test]
fn custom_config_is_accepted() {
let mut config = Config::new();
config.memory_pages_limit(1);
let engine = Engine::new(&config);
assert_eq!(engine.config().get_memory_pages_limit(), 1);
let module = Module::new(&wasm(r#"(module (memory (export "m") 1))"#)).unwrap();
let linker = Linker::new(&engine);
assert!(linker.instantiate(&module).is_ok());
let module = Module::new(&wasm(r#"(module (memory (export "m") 2))"#)).unwrap();
assert!(matches!(
linker.instantiate(&module),
Err(Error::Instantiation(_))
));
}