Skip to content

Commit e005e5a

Browse files
feat: initial exception support
Signed-off-by: Henry <mail@henrygressmann.de>
1 parent 9893a51 commit e005e5a

27 files changed

Lines changed: 744 additions & 103 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Added support for the WebAssembly function-references proposal
1313
- Added basic support for the WebAssembly garbage-collection proposal
14+
- Added support for the WebAssembly exception-handling proposal, including tags, `try_table`, `throw`, and `throw_ref`
1415
- Added `WasmValue::ty` and `WasmValue::matches_type`
1516
- Added `ValueLane` for mapping WebAssembly value types to their physical 32-bit, 64-bit, or 128-bit storage lane.
1617
- Added a `validate` feature to `tinywasm` and `tinywasm-parser` (enabled by default) to optionally skip wasmparser validation for faster parsing of trusted modules.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ TinyWasm targets non-JavaScript core proposals through [phase 3](https://github.
9696
| [**Custom Page Sizes**](https://github.com/WebAssembly/custom-page-sizes) | 🟢 | 0.9.0 |
9797
| [**Typed Function References**](https://github.com/WebAssembly/function-references) | 🟢 | `next` |
9898
| [**Garbage Collection**](https://github.com/WebAssembly/gc) | 🟢 | `next` |
99-
| [**Exception Handling**](https://github.com/WebAssembly/exception-handling) | 🌑 | - |
99+
| [**Exception Handling**](https://github.com/WebAssembly/exception-handling) | 🟢 | `next` |
100100
| [**Stack Switching**](https://github.com/WebAssembly/stack-switching) | 🌑 | - |
101101
| [**Compact Import Section**](https://github.com/WebAssembly/compact-import-section) | 🌑 | - |
102102
| [**Threads**](https://github.com/WebAssembly/threads) | 🌑 | - |

crates/cli/src/output.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ pub fn format_export_type(ty: ExportType<'_>) -> String {
7777
ExportType::Memory(ty) => format_memory_type(ty),
7878
ExportType::Table(ty) => format_table_type(ty),
7979
ExportType::Global(ty) => format_global_type(ty),
80+
ExportType::Tag(ty) => format!("tag {}", format_func_type(ty)),
8081
}
8182
}
8283

@@ -86,5 +87,6 @@ pub fn format_import_type(ty: ImportType<'_>) -> String {
8687
ImportType::Memory(ty) => format_memory_type(ty),
8788
ImportType::Table(ty) => format_table_type(ty),
8889
ImportType::Global(ty) => format_global_type(ty),
90+
ImportType::Tag(ty) => format!("tag {}", format_func_type(ty)),
8991
}
9092
}

crates/cli/src/wast_runner.rs

Lines changed: 134 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,53 +19,63 @@ const TEST_MAX_SUSPENSIONS: u32 = 1000;
1919

2020
#[derive(Default)]
2121
struct ModuleRegistry {
22-
modules: HashMap<String, ModuleInstance>,
23-
named_modules: HashMap<String, ModuleInstance>,
24-
last_module: Option<ModuleInstance>,
22+
definitions: HashMap<String, Module>,
23+
instances: HashMap<String, ModuleInstance>,
24+
registered: HashMap<String, ModuleInstance>,
25+
last_definition: Option<Module>,
26+
last_instance: Option<ModuleInstance>,
2527
}
2628

2729
impl ModuleRegistry {
28-
fn modules(&self) -> &HashMap<String, ModuleInstance> {
29-
&self.modules
30+
fn registered(&self) -> &HashMap<String, ModuleInstance> {
31+
&self.registered
3032
}
3133

32-
fn update_last_module(&mut self, module: ModuleInstance, name: Option<String>) {
33-
self.last_module = Some(module.clone());
34+
fn define(&mut self, module: Module, name: Option<String>) {
35+
self.last_definition = Some(module.clone());
3436
if let Some(name) = name {
35-
self.named_modules.insert(name, module);
37+
self.definitions.insert(name, module);
3638
}
3739
}
3840

39-
fn register(&mut self, name: String, module: ModuleInstance) {
41+
fn definition(&self, id: Option<wast::token::Id<'_>>) -> Option<Module> {
42+
match id {
43+
Some(id) => self.definitions.get(id.name()).cloned(),
44+
None => self.last_definition.clone(),
45+
}
46+
}
47+
48+
fn update_last_instance(&mut self, instance: ModuleInstance, name: Option<String>) {
49+
self.last_instance = Some(instance.clone());
50+
if let Some(name) = name {
51+
self.instances.insert(name, instance);
52+
}
53+
}
54+
55+
fn register(&mut self, name: String, id: Option<wast::token::Id<'_>>) -> bool {
56+
let Some(instance) = self.get(id) else { return false };
4057
debug!("registering module: {name}");
41-
self.modules.insert(name.clone(), module.clone());
42-
self.last_module = Some(module.clone());
43-
self.named_modules.insert(name, module);
58+
self.registered.insert(name, instance);
59+
true
4460
}
4561

4662
fn get_idx(&self, module_id: Option<wast::token::Id<'_>>) -> Option<u32> {
4763
match module_id {
48-
Some(module) => self
49-
.modules
50-
.get(module.name())
51-
.or_else(|| self.named_modules.get(module.name()))
52-
.map(ModuleInstance::id),
53-
None => self.last_module.as_ref().map(ModuleInstance::id),
64+
Some(module) => {
65+
self.registered.get(module.name()).or_else(|| self.instances.get(module.name())).map(ModuleInstance::id)
66+
}
67+
None => self.last_instance.as_ref().map(ModuleInstance::id),
5468
}
5569
}
5670

5771
fn get(&self, module_id: Option<wast::token::Id<'_>>) -> Option<ModuleInstance> {
5872
match module_id {
5973
Some(module_id) => {
60-
self.modules.get(module_id.name()).or_else(|| self.named_modules.get(module_id.name())).cloned()
74+
self.registered.get(module_id.name()).or_else(|| self.instances.get(module_id.name())).cloned()
6175
}
62-
None => self.last_module.clone(),
76+
None => self.last_instance.clone(),
6377
}
6478
}
65-
66-
fn last(&self) -> Option<ModuleInstance> {
67-
self.last_module.clone()
68-
}
6979
}
7080

7181
#[derive(Default)]
@@ -200,6 +210,11 @@ impl WastRunner {
200210
Ok(imports)
201211
}
202212

213+
fn instantiate_module(store: &mut Store, registry: &ModuleRegistry, module: &Module) -> Result<ModuleInstance> {
214+
let imports = Self::imports(store, registry.registered())?;
215+
Ok(ModuleInstance::instantiate(store, module, Some(&imports))?)
216+
}
217+
203218
pub fn run_file(&mut self, file: TestFile<'_>) -> Result<()> {
204219
let test_group = self.test_group(file.name(), file.parent());
205220
let wast_raw = file.raw();
@@ -213,51 +228,72 @@ impl WastRunner {
213228
for (i, directive) in directives.into_iter().enumerate() {
214229
let span = directive.span();
215230
use wast::WastDirective::{
216-
AssertExhaustion, AssertInvalid, AssertMalformed, AssertReturn, AssertTrap, AssertUnlinkable, Invoke,
217-
Module as Wat, ModuleDefinition, Register,
231+
AssertException, AssertExhaustion, AssertInvalid, AssertMalformed, AssertReturn, AssertTrap,
232+
AssertUnlinkable, Invoke, Module as Wat, ModuleDefinition, ModuleInstance as Instance, Register,
218233
};
219234

220235
match directive {
221-
Register { span, name, .. } => {
222-
let Some(last) = module_registry.last() else {
236+
Register { span, name, module } => {
237+
if !module_registry.register(name.to_string(), module) {
223238
test_group.add_result(
224239
&format!("Register({i})"),
225240
span.linecol_in(wast_raw),
226-
Err(eyre!("no module to register")),
241+
Err(eyre!("module instance to register was not found")),
227242
);
228243
continue;
229-
};
230-
module_registry.register(name.to_string(), last);
244+
}
231245
test_group.add_result(&format!("Register({i})"), span.linecol_in(wast_raw), Ok(()));
232246
}
233247
Wat(module) => {
234248
let result = catch_unwind_silent(|| {
235-
let (name, bytes) = encode_quote_wat(module);
236-
let module = parse_module_bytes(&bytes).expect("failed to parse module bytes");
237-
let imports = Self::imports(&mut store, module_registry.modules()).unwrap();
238-
let module_instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))
249+
let (name, module) = parse_quote_module(module).expect("failed to parse module bytes");
250+
let instance = Self::instantiate_module(&mut store, &module_registry, &module)
239251
.expect("failed to instantiate module");
240-
(name, module_instance)
252+
(name, instance)
241253
})
242254
.map_err(|e| eyre!("failed to parse wat module: {}", try_downcast_panic(e)));
243255

244256
match &result {
245257
Err(err) => debug!("failed to parse module: {err:?}"),
246-
Ok((name, module)) => module_registry.update_last_module(module.clone(), name.clone()),
258+
Ok((name, instance)) => module_registry.update_last_instance(instance.clone(), name.clone()),
247259
};
248260

249261
test_group.add_result(&format!("Wat({i})"), span.linecol_in(wast_raw), result.map(|_| ()));
250262
}
251263
ModuleDefinition(module) => {
264+
let result =
265+
catch_unwind_silent(|| parse_quote_module(module).expect("failed to parse module definition"))
266+
.map_err(|err| eyre!("failed to parse module definition: {}", try_downcast_panic(err)));
267+
268+
if let Ok((name, module)) = &result {
269+
module_registry.define(module.clone(), name.clone());
270+
}
271+
272+
test_group.add_result(
273+
&format!("ModuleDefinition({i})"),
274+
span.linecol_in(wast_raw),
275+
result.map(|_| ()),
276+
);
277+
}
278+
Instance { span, instance, module } => {
279+
let name = instance.map(|id| id.name().to_string());
252280
let result = catch_unwind_silent(|| {
253-
let (_, bytes) = encode_quote_wat(module);
254-
parse_module_bytes(&bytes)
281+
let module = module_registry
282+
.definition(module)
283+
.ok_or_else(|| eyre!("module definition was not found"))?;
284+
Self::instantiate_module(&mut store, &module_registry, &module)
255285
})
256-
.map_err(|err| eyre!("failed to parse module definition: {}", try_downcast_panic(err)))
257-
.and_then(|result| result)
258-
.map(|_| ());
286+
.map_err(|err| eyre!("failed to instantiate module definition: {}", try_downcast_panic(err)))
287+
.and_then(|result| result);
259288

260-
test_group.add_result(&format!("ModuleDefinition({i})"), span.linecol_in(wast_raw), result);
289+
if let Ok(instance) = &result {
290+
module_registry.update_last_instance(instance.clone(), name);
291+
}
292+
test_group.add_result(
293+
&format!("ModuleInstance({i})"),
294+
span.linecol_in(wast_raw),
295+
result.map(|_| ()),
296+
);
261297
}
262298
AssertMalformed { span, mut module, message } => {
263299
let Ok(encoded) = module.encode() else {
@@ -330,7 +366,7 @@ impl WastRunner {
330366
wast::WastExecute::Wat(mut wat) => {
331367
let module = parse_module_bytes(&wat.encode().expect("failed to encode module"))
332368
.expect("failed to parse module");
333-
let imports = Self::imports(&mut store, module_registry.modules()).unwrap();
369+
let imports = Self::imports(&mut store, module_registry.registered()).unwrap();
334370
ModuleInstance::instantiate(&mut store, &module, Some(&imports))?;
335371
return Ok(());
336372
}
@@ -371,11 +407,37 @@ impl WastRunner {
371407
),
372408
}
373409
}
410+
AssertException { exec, span } => {
411+
let res: Result<tinywasm::Result<()>, _> = catch_unwind_silent(|| {
412+
let invoke = match exec {
413+
wast::WastExecute::Wat(mut wat) => {
414+
let module = parse_module_bytes(&wat.encode().expect("failed to encode module"))
415+
.expect("failed to parse module");
416+
let imports = Self::imports(&mut store, module_registry.registered()).unwrap();
417+
ModuleInstance::instantiate(&mut store, &module, Some(&imports))?;
418+
return Ok(());
419+
}
420+
wast::WastExecute::Get { .. } => panic!("get not supported"),
421+
wast::WastExecute::Invoke(invoke) => invoke,
422+
};
423+
let module = module_registry.get_idx(invoke.module);
424+
let args =
425+
convert_wastargs(invoke.args).map_err(|err| tinywasm::Error::Other(err.to_string()))?;
426+
exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ())
427+
});
428+
let result = match res {
429+
Err(err) => Err(eyre!("test panicked: {}", try_downcast_panic(err))),
430+
Ok(Err(tinywasm::Error::Exception(_))) => Ok(()),
431+
Ok(Err(err)) => Err(eyre!("expected exception, got: {err:?}")),
432+
Ok(Ok(())) => Err(eyre!("expected exception, got Ok")),
433+
};
434+
test_group.add_result(&format!("AssertException({i})"), span.linecol_in(wast_raw), result);
435+
}
374436
AssertUnlinkable { mut module, span, message } => {
375437
let res = catch_unwind_silent(|| {
376438
let module = parse_module_bytes(&module.encode().expect("failed to encode module"))
377439
.expect("failed to parse module");
378-
let imports = Self::imports(&mut store, module_registry.modules()).unwrap();
440+
let imports = Self::imports(&mut store, module_registry.registered()).unwrap();
379441
ModuleInstance::instantiate(&mut store, &module, Some(&imports))
380442
});
381443
match res {
@@ -729,6 +791,11 @@ fn parse_module_bytes(bytes: &[u8]) -> Result<Module> {
729791
Ok(tinywasm::parse_bytes(bytes)?)
730792
}
731793

794+
fn parse_quote_module(module: QuoteWat) -> Result<(Option<String>, Module)> {
795+
let (name, bytes) = encode_quote_wat(module);
796+
Ok((name, parse_module_bytes(&bytes)?))
797+
}
798+
732799
fn convert_wastargs(args: Vec<wast::WastArg>) -> Result<Vec<WasmValue>> {
733800
args.into_iter().map(wastarg2tinywasmvalue).collect()
734801
}
@@ -947,4 +1014,26 @@ mod tests {
9471014
let mut runner = WastRunner::new();
9481015
runner.run_paths(&[path]).unwrap();
9491016
}
1017+
1018+
#[test]
1019+
fn runs_module_definition_and_instance_directives() {
1020+
let dir = tempfile::tempdir().unwrap();
1021+
let path = dir.path().join("instance.wast");
1022+
std::fs::write(
1023+
&path,
1024+
r#"
1025+
(module definition $M (global (export "g") i32 (i32.const 42)))
1026+
(module instance $I $M)
1027+
(register "I" $I)
1028+
(module
1029+
(import "I" "g" (global $g i32))
1030+
(func (export "get") (result i32) global.get $g))
1031+
(assert_return (invoke "get") (i32.const 42))
1032+
"#,
1033+
)
1034+
.unwrap();
1035+
1036+
let mut runner = WastRunner::new();
1037+
runner.run_paths(&[path]).unwrap();
1038+
}
9501039
}

crates/parser/src/conversion.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,7 @@ pub(crate) fn convert_module_import(import: wasmparser::Import<'_>) -> Result<Im
7676
wasmparser::TypeRef::Global(ty) => {
7777
ImportKind::Global(GlobalType::new(convert_valtype(&ty.content_type)?, ty.mutable))
7878
}
79-
wasmparser::TypeRef::Tag(ty) => {
80-
return Err(crate::ParseError::UnsupportedOperator(format!("Unsupported import kind: {ty:?}")));
81-
}
79+
wasmparser::TypeRef::Tag(ty) => ImportKind::Tag(convert_tag_type(ty)),
8280
_ => {
8381
return Err(crate::ParseError::UnsupportedOperator(format!("Unsupported import kind: {:?}", import.ty)));
8482
}
@@ -116,14 +114,19 @@ pub(crate) fn convert_module_export(export: wasmparser::Export<'_>) -> Result<Ex
116114
wasmparser::ExternalKind::Table => ExternalKind::Table,
117115
wasmparser::ExternalKind::Memory => ExternalKind::Memory,
118116
wasmparser::ExternalKind::Global => ExternalKind::Global,
119-
wasmparser::ExternalKind::Tag | wasmparser::ExternalKind::FuncExact => {
117+
wasmparser::ExternalKind::Tag => ExternalKind::Tag,
118+
wasmparser::ExternalKind::FuncExact => {
120119
return Err(crate::ParseError::UnsupportedOperator(format!("Unsupported export kind: {:?}", export.kind)));
121120
}
122121
};
123122

124123
Ok(Export { index: export.index, name: Box::from(export.name), kind })
125124
}
126125

126+
pub(crate) const fn convert_tag_type(ty: wasmparser::TagType) -> TagType {
127+
TagType::new(ty.func_type_idx)
128+
}
129+
127130
fn extend_local_types(local_types: &mut Vec<ValueLane>, count: u32, ty: wasmparser::ValType) -> Result<()> {
128131
let size = value_lane(ty);
129132
let count =

crates/parser/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ impl Parser {
189189
| WasmFeatures::SIMD
190190
| WasmFeatures::MEMORY64
191191
| WasmFeatures::CUSTOM_PAGE_SIZES
192+
| WasmFeatures::EXCEPTIONS
192193
| WasmFeatures::WIDE_ARITHMETIC;
193194
self.options.validation().then(|| Validator::new_with_features(features))
194195
}

crates/parser/src/macros.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ pub(crate) mod visit {
160160
(@@tail_call $($rest:tt)* ) => {};
161161
(@@function_references $($rest:tt)* ) => {};
162162
(@@gc $($rest:tt)* ) => {};
163+
(@@exceptions $($rest:tt)* ) => {};
163164

164165
(@@$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*)) => {
165166
fn $visit(&mut self $($(,_: $argty)*)?) -> Self::Output {

0 commit comments

Comments
 (0)