1use std::collections::{BTreeMap, HashMap};
2use std::fmt::{Display, Formatter};
3use std::fs::canonicalize;
4use std::panic::{self, AssertUnwindSafe};
5use std::path::PathBuf;
6use std::time::Duration;
7
8use eyre::{Context, Result, bail, eyre};
9use log::{debug, error};
10use tinywasm::types::{ExternRef, FuncRef, MemoryType, TableType, WasmType, WasmValue};
11use tinywasm::{ExecProgress, Global, HostFunction, Imports, Memory, Module, ModuleInstance, Store, Table};
12use wast::{QuoteWat, core::AbstractHeapType};
13
14const TEST_TIME_SLICE: Duration = Duration::from_millis(20);
15const TEST_MAX_SUSPENSIONS: u32 = 1000;
16
17#[derive(Default)]
18struct ModuleRegistry {
19 modules: HashMap<String, ModuleInstance>,
20 named_modules: HashMap<String, ModuleInstance>,
21 last_module: Option<ModuleInstance>,
22}
23
24impl ModuleRegistry {
25 fn modules(&self) -> &HashMap<String, ModuleInstance> {
26 &self.modules
27 }
28
29 fn update_last_module(&mut self, module: ModuleInstance, name: Option<String>) {
30 self.last_module = Some(module.clone());
31 if let Some(name) = name {
32 self.named_modules.insert(name, module);
33 }
34 }
35
36 fn register(&mut self, name: String, module: ModuleInstance) {
37 debug!("registering module: {name}");
38 self.modules.insert(name.clone(), module.clone());
39 self.last_module = Some(module.clone());
40 self.named_modules.insert(name, module);
41 }
42
43 fn get_idx(&self, module_id: Option<wast::token::Id<'_>>) -> Option<u32> {
44 match module_id {
45 Some(module) => self
46 .modules
47 .get(module.name())
48 .or_else(|| self.named_modules.get(module.name()))
49 .map(ModuleInstance::id),
50 None => self.last_module.as_ref().map(ModuleInstance::id),
51 }
52 }
53
54 fn get(&self, module_id: Option<wast::token::Id<'_>>) -> Option<ModuleInstance> {
55 match module_id {
56 Some(module_id) => {
57 self.modules.get(module_id.name()).or_else(|| self.named_modules.get(module_id.name())).cloned()
58 }
59 None => self.last_module.clone(),
60 }
61 }
62
63 fn last(&self) -> Option<ModuleInstance> {
64 self.last_module.clone()
65 }
66}
67
68#[derive(Default)]
69pub struct WastRunner(BTreeMap<String, TestGroup>);
70
71#[derive(Clone, Debug)]
72pub struct GroupResult {
73 pub name: String,
74 pub file: String,
75 pub passed: usize,
76 pub failed: usize,
77}
78
79impl WastRunner {
80 pub fn new() -> Self {
81 Self::default()
82 }
83
84 pub fn run_paths(&mut self, tests: &[PathBuf]) -> Result<()> {
85 for path in expand_paths(tests)? {
86 let contents =
87 std::fs::read_to_string(&path).context(format!("failed to read file: {}", path.to_string_lossy()))?;
88
89 let file = TestFile {
90 contents: &contents,
91 name: path.to_string_lossy().to_string(),
92 parent: canonicalize(&path)?.to_string_lossy().to_string(),
93 };
94
95 self.run_file(file)?;
96 }
97
98 self.print_errors();
99 if self.failed() {
100 anstream::println!("{self}");
101 Err(eyre!("failed one or more tests"))
102 } else {
103 anstream::println!("{self}");
104 Ok(())
105 }
106 }
107
108 pub fn set_log_level(level: log::LevelFilter) {
109 let _ = pretty_env_logger::formatted_builder().filter_level(level).try_init();
110 }
111
112 pub fn failed(&self) -> bool {
113 self.0.values().any(|group| group.stats().1 > 0)
114 }
115
116 pub fn print_errors(&self) {
117 for group in self.0.values() {
118 for test in &group.tests {
119 if let Err(err) = &test.result {
120 eprintln!(
121 "{}:{}:{} {} failed: {}",
122 group.file,
123 test.linecol.0 + 1,
124 test.linecol.1 + 1,
125 test.name,
126 err
127 );
128 }
129 }
130 }
131 }
132
133 pub fn group_results(&self) -> Vec<GroupResult> {
134 self.0
135 .iter()
136 .map(|(name, group)| {
137 let (passed, failed) = group.stats();
138 GroupResult { name: name.clone(), file: group.file.clone(), passed, failed }
139 })
140 .collect()
141 }
142
143 fn test_group(&mut self, name: &str, file: &str) -> &mut TestGroup {
144 self.0.entry(name.to_string()).or_insert_with(|| TestGroup::new(file))
145 }
146
147 pub fn run_files<'a>(&mut self, tests: impl IntoIterator<Item = TestFile<'a>>) -> Result<()> {
148 for file in tests {
149 self.run_file(file)?;
150 }
151 Ok(())
152 }
153
154 fn imports(store: &mut Store, modules: &HashMap<String, ModuleInstance>) -> Result<Imports> {
155 let mut imports = Imports::new();
156
157 let table = Table::new(
158 store,
159 TableType::new(WasmType::RefFunc, 10, Some(20)),
160 WasmValue::default_for(WasmType::RefFunc),
161 )?;
162 let memory = Memory::new(store, MemoryType::default().with_page_count_initial(1).with_page_count_max(Some(2)))?;
163 let global_i32 =
164 Global::new(store, tinywasm::types::GlobalType::new(WasmType::I32, false), WasmValue::I32(666))?;
165 let global_i64 =
166 Global::new(store, tinywasm::types::GlobalType::new(WasmType::I64, false), WasmValue::I64(666))?;
167 let global_f32 =
168 Global::new(store, tinywasm::types::GlobalType::new(WasmType::F32, false), WasmValue::F32(666.6))?;
169 let global_f64 =
170 Global::new(store, tinywasm::types::GlobalType::new(WasmType::F64, false), WasmValue::F64(666.6))?;
171
172 imports
173 .define("spectest", "memory", memory)
174 .define("spectest", "table", table)
175 .define("spectest", "global_i32", global_i32)
176 .define("spectest", "global_i64", global_i64)
177 .define("spectest", "global_f32", global_f32)
178 .define("spectest", "global_f64", global_f64)
179 .define("spectest", "print", HostFunction::from(store, |_ctx: tinywasm::FuncContext, (): ()| Ok(())))
180 .define("spectest", "print_i32", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: i32| Ok(())))
181 .define("spectest", "print_i64", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: i64| Ok(())))
182 .define("spectest", "print_f32", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: f32| Ok(())))
183 .define("spectest", "print_f64", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: f64| Ok(())))
184 .define(
185 "spectest",
186 "print_i32_f32",
187 HostFunction::from(store, |_ctx: tinywasm::FuncContext, _args: (i32, f32)| Ok(())),
188 )
189 .define(
190 "spectest",
191 "print_f64_f64",
192 HostFunction::from(store, |_ctx: tinywasm::FuncContext, _args: (f64, f64)| Ok(())),
193 );
194
195 for (name, module) in modules {
196 imports.link_module(name, module.clone())?;
197 }
198
199 Ok(imports)
200 }
201
202 pub fn run_file(&mut self, file: TestFile<'_>) -> Result<()> {
203 let test_group = self.test_group(file.name(), file.parent());
204 let wast_raw = file.raw();
205 let wast = file.wast()?;
206 let directives = wast.directives()?;
207
208 let mut store = Store::default();
209 let mut module_registry = ModuleRegistry::default();
210
211 println!("running {} tests for group: {}", directives.len(), file.name());
212 for (i, directive) in directives.into_iter().enumerate() {
213 let span = directive.span();
214 use wast::WastDirective::{
215 AssertExhaustion, AssertInvalid, AssertMalformed, AssertReturn, AssertTrap, AssertUnlinkable, Invoke,
216 Module as Wat, Register,
217 };
218
219 match directive {
220 Register { span, name, .. } => {
221 let Some(last) = module_registry.last() else {
222 test_group.add_result(
223 &format!("Register({i})"),
224 span.linecol_in(wast_raw),
225 Err(eyre!("no module to register")),
226 );
227 continue;
228 };
229 module_registry.register(name.to_string(), last);
230 test_group.add_result(&format!("Register({i})"), span.linecol_in(wast_raw), Ok(()));
231 }
232 Wat(module) => {
233 let result = catch_unwind_silent(|| {
234 let (name, bytes) = encode_quote_wat(module);
235 let module = parse_module_bytes(&bytes).expect("failed to parse module bytes");
236 let imports = Self::imports(&mut store, module_registry.modules()).unwrap();
237 let module_instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))
238 .expect("failed to instantiate module");
239 (name, module_instance)
240 })
241 .map_err(|e| eyre!("failed to parse wat module: {}", try_downcast_panic(e)));
242
243 match &result {
244 Err(err) => debug!("failed to parse module: {err:?}"),
245 Ok((name, module)) => module_registry.update_last_module(module.clone(), name.clone()),
246 };
247
248 test_group.add_result(&format!("Wat({i})"), span.linecol_in(wast_raw), result.map(|_| ()));
249 }
250 AssertMalformed { span, mut module, message } => {
251 let Ok(encoded) = module.encode() else {
252 test_group.add_result(&format!("AssertMalformed({i})"), span.linecol_in(wast_raw), Ok(()));
253 continue;
254 };
255 let res = catch_unwind_silent(|| parse_module_bytes(&encoded))
256 .map_err(|e| eyre!("failed to parse module (expected): {}", try_downcast_panic(e)))
257 .and_then(|res| res);
258 test_group.add_result(
259 &format!("AssertMalformed({i})"),
260 span.linecol_in(wast_raw),
261 match res {
262 Ok(_) => {
263 if message == "zero byte expected"
264 || message == "integer representation too long"
265 || message == "zero flag expected"
266 {
267 continue;
268 }
269 Err(eyre!("expected module to be malformed: {message}"))
270 }
271 Err(_) => Ok(()),
272 },
273 );
274 }
275 AssertInvalid { span, mut module, message } => {
276 if ["multiple memories", "type mismatch"].contains(&message) {
277 test_group.add_result(&format!("AssertInvalid({i})"), span.linecol_in(wast_raw), Ok(()));
278 continue;
279 }
280 let res = catch_unwind_silent(move || parse_module_bytes(&module.encode().unwrap()))
281 .map_err(|e| eyre!("failed to parse module (invalid): {}", try_downcast_panic(e)))
282 .and_then(|res| res);
283 test_group.add_result(
284 &format!("AssertInvalid({i})"),
285 span.linecol_in(wast_raw),
286 match res {
287 Ok(_) => Err(eyre!("expected module to be invalid")),
288 Err(_) => Ok(()),
289 },
290 );
291 }
292 AssertExhaustion { call, message, span } => {
293 let module = module_registry.get_idx(call.module);
294 let args = convert_wastargs(call.args)?;
295 let res =
296 catch_unwind_silent(|| exec_fn_instance(module, &mut store, call.name, &args).map(|_| ()));
297 let Ok(Err(tinywasm::Error::Trap(trap))) = res else {
298 test_group.add_result(
299 &format!("AssertExhaustion({i})"),
300 span.linecol_in(wast_raw),
301 Err(eyre!("expected trap")),
302 );
303 continue;
304 };
305 if !message.starts_with(trap.message()) && !trap.message().starts_with(message) {
306 test_group.add_result(
307 &format!("AssertExhaustion({i})"),
308 span.linecol_in(wast_raw),
309 Err(eyre!("expected trap: {}, got: {}", message, trap.message())),
310 );
311 continue;
312 }
313 test_group.add_result(&format!("AssertExhaustion({i})"), span.linecol_in(wast_raw), Ok(()));
314 }
315 AssertTrap { exec, message, span } => {
316 let res: Result<tinywasm::Result<()>, _> = catch_unwind_silent(|| {
317 let invoke = match exec {
318 wast::WastExecute::Wat(mut wat) => {
319 let module = parse_module_bytes(&wat.encode().expect("failed to encode module"))
320 .expect("failed to parse module");
321 let imports = Self::imports(&mut store, module_registry.modules()).unwrap();
322 ModuleInstance::instantiate(&mut store, &module, Some(imports))?;
323 return Ok(());
324 }
325 wast::WastExecute::Get { .. } => panic!("get not supported"),
326 wast::WastExecute::Invoke(invoke) => invoke,
327 };
328 let module = module_registry.get_idx(invoke.module);
329 let args =
330 convert_wastargs(invoke.args).map_err(|err| tinywasm::Error::Other(err.to_string()))?;
331 exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ())
332 });
333 match res {
334 Err(err) => test_group.add_result(
335 &format!("AssertTrap({i})"),
336 span.linecol_in(wast_raw),
337 Err(eyre!("test panicked: {}", try_downcast_panic(err))),
338 ),
339 Ok(Err(tinywasm::Error::Trap(trap))) => {
340 if !message.starts_with(trap.message()) && !trap.message().starts_with(message) {
341 test_group.add_result(
342 &format!("AssertTrap({i})"),
343 span.linecol_in(wast_raw),
344 Err(eyre!("expected trap: {}, got: {}", message, trap.message())),
345 );
346 continue;
347 }
348 test_group.add_result(&format!("AssertTrap({i})"), span.linecol_in(wast_raw), Ok(()));
349 }
350 Ok(Err(err)) => test_group.add_result(
351 &format!("AssertTrap({i})"),
352 span.linecol_in(wast_raw),
353 Err(eyre!("expected trap, {}, got: {:?}", message, err)),
354 ),
355 Ok(Ok(())) => test_group.add_result(
356 &format!("AssertTrap({i})"),
357 span.linecol_in(wast_raw),
358 Err(eyre!("expected trap {}, got Ok", message)),
359 ),
360 }
361 }
362 AssertUnlinkable { mut module, span, message } => {
363 let res = catch_unwind_silent(|| {
364 let module = parse_module_bytes(&module.encode().expect("failed to encode module"))
365 .expect("failed to parse module");
366 let imports = Self::imports(&mut store, module_registry.modules()).unwrap();
367 ModuleInstance::instantiate(&mut store, &module, Some(imports))
368 });
369 match res {
370 Err(err) => test_group.add_result(
371 &format!("AssertUnlinkable({i})"),
372 span.linecol_in(wast_raw),
373 Err(eyre!("test panicked: {}", try_downcast_panic(err))),
374 ),
375 Ok(Err(tinywasm::Error::Linker(err))) => {
376 if err.message() != message
377 && (err.message() == "memory types incompatible"
378 && message != "incompatible import type")
379 {
380 test_group.add_result(
381 &format!("AssertUnlinkable({i})"),
382 span.linecol_in(wast_raw),
383 Err(eyre!("expected linker error: {}, got: {}", message, err.message())),
384 );
385 continue;
386 }
387 test_group.add_result(&format!("AssertUnlinkable({i})"), span.linecol_in(wast_raw), Ok(()));
388 }
389 Ok(Err(err)) => test_group.add_result(
390 &format!("AssertUnlinkable({i})"),
391 span.linecol_in(wast_raw),
392 Err(eyre!("expected linker error, {}, got: {:?}", message, err)),
393 ),
394 Ok(Ok(_)) => test_group.add_result(
395 &format!("AssertUnlinkable({i})"),
396 span.linecol_in(wast_raw),
397 Err(eyre!("expected linker error {}, got Ok", message)),
398 ),
399 }
400 }
401 Invoke(invoke) => {
402 let name = invoke.name;
403 let res: Result<Result<()>, _> = catch_unwind_silent(|| {
404 let args = convert_wastargs(invoke.args)?;
405 let module = module_registry.get_idx(invoke.module);
406 exec_fn_instance(module, &mut store, invoke.name, &args).map_err(|e| {
407 error!("failed to execute function: {e:?}");
408 e
409 })?;
410 Ok(())
411 });
412 let res = res.map_err(|e| eyre!("test panicked: {}", try_downcast_panic(e))).and_then(|r| r);
413 test_group.add_result(&format!("Invoke({name}-{i})"), span.linecol_in(wast_raw), res);
414 }
415 AssertReturn { span, exec, results } => {
416 let expected_alternatives = match convert_wastret(results.into_iter()) {
417 Err(err) => {
418 test_group.add_result(
419 &format!("AssertReturn(unsupported-{i})"),
420 span.linecol_in(wast_raw),
421 Err(eyre!("failed to convert expected results: {err:?}")),
422 );
423 continue;
424 }
425 Ok(expected) => expected,
426 };
427
428 let invoke = match match exec {
429 wast::WastExecute::Wat(_) => Err(eyre!("wat not supported")),
430 wast::WastExecute::Get { module: module_id, global, .. } => {
431 let Some(module) = module_registry.get(module_id) else {
432 test_group.add_result(
433 &format!("AssertReturn(unsupported-{i})"),
434 span.linecol_in(wast_raw),
435 Err(eyre!("no module to get global from")),
436 );
437 continue;
438 };
439 let module_global = match module.global_get(&store, global) {
440 Ok(value) => value,
441 Err(err) => {
442 test_group.add_result(
443 &format!("AssertReturn(unsupported-{i})"),
444 span.linecol_in(wast_raw),
445 Err(eyre!("failed to get global: {err:?}")),
446 );
447 continue;
448 }
449 };
450 let expected = expected_alternatives
451 .iter()
452 .filter_map(|alts| alts.first())
453 .find(|exp| module_global.eq_loose(exp));
454 if expected.is_none() {
455 test_group.add_result(
456 &format!("AssertReturn(unsupported-{i})"),
457 span.linecol_in(wast_raw),
458 Err(eyre!(
459 "global value did not match any expected alternative: {:?}",
460 module_global
461 )),
462 );
463 continue;
464 }
465 test_group.add_result(
466 &format!("AssertReturn({global}-{i})"),
467 span.linecol_in(wast_raw),
468 Ok(()),
469 );
470 continue;
471 }
472 wast::WastExecute::Invoke(invoke) => Ok(invoke),
473 } {
474 Ok(invoke) => invoke,
475 Err(err) => {
476 test_group.add_result(
477 &format!("AssertReturn(unsupported-{i})"),
478 span.linecol_in(wast_raw),
479 Err(eyre!("unsupported directive: {err:?}")),
480 );
481 continue;
482 }
483 };
484
485 let invoke_name = invoke.name;
486 let res: Result<Result<()>, _> = catch_unwind_silent(|| {
487 let args = convert_wastargs(invoke.args)?;
488 let module = module_registry.get_idx(invoke.module);
489 let outcomes = exec_fn_instance(module, &mut store, invoke.name, &args).map_err(|e| {
490 error!("failed to execute function: {e:?}");
491 e
492 })?;
493 if !expected_alternatives.iter().any(|expected| expected.len() == outcomes.len()) {
494 return Err(eyre!(
495 "expected {} results, got {}",
496 expected_alternatives.first().map_or(0, |v| v.len()),
497 outcomes.len()
498 ));
499 }
500 if expected_alternatives.iter().any(|expected| {
501 expected.len() == outcomes.len()
502 && outcomes.iter().zip(expected.iter()).all(|(outcome, exp)| outcome.eq_loose(exp))
503 }) {
504 Ok(())
505 } else {
506 Err(eyre!("results did not match any expected alternative"))
507 }
508 });
509
510 let res = res.map_err(|e| eyre!("test panicked: {}", try_downcast_panic(e))).and_then(|r| r);
511 test_group.add_result(&format!("AssertReturn({invoke_name}-{i})"), span.linecol_in(wast_raw), res);
512 }
513 _ => test_group.add_result(
514 &format!("Unknown({i})"),
515 span.linecol_in(wast_raw),
516 Err(eyre!("unsupported directive")),
517 ),
518 }
519 }
520
521 Ok(())
522 }
523}
524
525impl Display for WastRunner {
526 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
527 use owo_colors::OwoColorize;
528
529 let mut total_passed = 0;
530 let mut total_failed = 0;
531
532 for group in self.group_results() {
533 total_passed += group.passed;
534 total_failed += group.failed;
535
536 writeln!(f, "{}", group.name.bold().underline())?;
537 writeln!(f, " Tests Passed: {}", group.passed.to_string().green())?;
538 if group.failed != 0 {
539 writeln!(f, " Tests Failed: {}", group.failed.to_string().red())?;
540 }
541 }
542
543 writeln!(f, "\n{}", "Total Test Summary:".bold().underline())?;
544 writeln!(f, " Total Tests: {}", total_passed + total_failed)?;
545 writeln!(f, " Total Passed: {}", total_passed.to_string().green())?;
546 writeln!(f, " Total Failed: {}", total_failed.to_string().red())?;
547 Ok(())
548 }
549}
550
551#[derive(Debug)]
552struct TestGroup {
553 tests: Vec<TestCase>,
554 file: String,
555}
556
557impl TestGroup {
558 fn new(file: &str) -> Self {
559 Self { tests: Vec::new(), file: file.to_string() }
560 }
561
562 fn stats(&self) -> (usize, usize) {
563 let mut passed = 0;
564 let mut failed = 0;
565 for test in &self.tests {
566 match test.result {
567 Ok(()) => passed += 1,
568 Err(_) => failed += 1,
569 }
570 }
571 (passed, failed)
572 }
573
574 fn add_result(&mut self, name: &str, linecol: (usize, usize), result: Result<()>) {
575 self.tests.push(TestCase { name: name.to_string(), linecol, result });
576 }
577}
578
579#[derive(Debug)]
580struct TestCase {
581 name: String,
582 linecol: (usize, usize),
583 result: Result<()>,
584}
585
586fn expand_paths(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
587 let mut files = Vec::new();
588 for path in paths {
589 if path.is_dir() {
590 for entry in std::fs::read_dir(path)? {
591 let entry = entry?;
592 let path = entry.path();
593 if path.extension().is_some_and(|ext| ext == "wast") {
594 files.push(path);
595 }
596 }
597 } else {
598 files.push(path.clone());
599 }
600 }
601 files.sort();
602 Ok(files)
603}
604
605#[derive(Debug)]
606pub struct TestFile<'a> {
607 pub name: String,
608 pub contents: &'a str,
609 pub parent: String,
610}
611
612impl<'a> TestFile<'a> {
613 pub fn name(&self) -> &str {
614 &self.name
615 }
616
617 pub fn raw(&self) -> &'a str {
618 self.contents
619 }
620
621 pub fn parent(&self) -> &str {
622 &self.parent
623 }
624
625 pub fn wast(&self) -> wast::parser::Result<WastBuffer<'a>> {
626 let mut lexer = wast::lexer::Lexer::new(self.contents);
627 lexer.allow_confusing_unicode(true);
628 let parse_buffer = wast::parser::ParseBuffer::new_with_lexer(lexer)?;
629 Ok(WastBuffer { buffer: parse_buffer })
630 }
631}
632
633pub struct WastBuffer<'a> {
634 buffer: wast::parser::ParseBuffer<'a>,
635}
636
637impl<'a> WastBuffer<'a> {
638 pub fn directives(&'a self) -> wast::parser::Result<Vec<wast::WastDirective<'a>>> {
639 Ok(wast::parser::parse::<wast::Wast<'a>>(&self.buffer)?.directives)
640 }
641}
642
643fn exec_with_budget(
644 func: &tinywasm::Function,
645 store: &mut Store,
646 args: &[WasmValue],
647) -> Result<Vec<WasmValue>, tinywasm::Error> {
648 let mut exec = func.call_resumable(store, args)?;
649 for _ in 0..TEST_MAX_SUSPENSIONS {
650 match exec.resume_with_time_budget(TEST_TIME_SLICE)? {
651 ExecProgress::Completed(values) => return Ok(values),
652 ExecProgress::Suspended => {}
653 }
654 }
655 Err(tinywasm::Error::Other(format!(
656 "testsuite execution timed out after {} time slices of {:?}",
657 TEST_MAX_SUSPENSIONS, TEST_TIME_SLICE
658 )))
659}
660
661fn try_downcast_panic(panic: Box<dyn std::any::Any + Send>) -> String {
662 let info = panic.downcast_ref::<panic::PanicHookInfo>().map(ToString::to_string);
663 let info_string = panic.downcast_ref::<String>().cloned();
664 let info_str = panic.downcast::<&str>().ok().map(|s| *s);
665 info.unwrap_or_else(|| info_str.unwrap_or(&info_string.unwrap_or("unknown panic".to_owned())).to_string())
666}
667
668fn exec_fn_instance(
669 instance: Option<u32>,
670 store: &mut Store,
671 name: &str,
672 args: &[WasmValue],
673) -> Result<Vec<WasmValue>, tinywasm::Error> {
674 let Some(instance) = instance else {
675 return Err(tinywasm::Error::Other("no instance found".to_string()));
676 };
677 let Some(instance) = store.get_module_instance(instance) else {
678 return Err(tinywasm::Error::Other("no instance found".to_string()));
679 };
680 let func = instance.func_untyped(store, name)?;
681 exec_with_budget(&func, store, args)
682}
683
684fn catch_unwind_silent<R>(f: impl FnOnce() -> R) -> std::thread::Result<R> {
685 let prev_hook = panic::take_hook();
686 panic::set_hook(Box::new(|_| {}));
687 let result = panic::catch_unwind(AssertUnwindSafe(f));
688 panic::set_hook(prev_hook);
689 result
690}
691
692fn encode_quote_wat(module: QuoteWat) -> (Option<String>, Vec<u8>) {
693 match module {
694 QuoteWat::QuoteModule(_, quoted_wat) => {
695 let wat = quoted_wat
696 .iter()
697 .map(|(_, s)| std::str::from_utf8(s).expect("failed to convert wast to utf8"))
698 .collect::<Vec<_>>()
699 .join("\n");
700 let lexer = wast::lexer::Lexer::new(&wat);
701 let buf = wast::parser::ParseBuffer::new_with_lexer(lexer).expect("failed to create parse buffer");
702 let mut wat_data = wast::parser::parse::<wast::Wat>(&buf).expect("failed to parse wat");
703 (None, wat_data.encode().expect("failed to encode module"))
704 }
705 QuoteWat::Wat(mut wat) => {
706 let wast::Wat::Module(ref module) = wat else { unimplemented!("Not supported") };
707 (module.id.map(|id| id.name().to_string()), wat.encode().expect("failed to encode module"))
708 }
709 QuoteWat::QuoteComponent(..) => unimplemented!("components are not supported"),
710 }
711}
712
713fn parse_module_bytes(bytes: &[u8]) -> Result<Module> {
714 Ok(tinywasm::parse_bytes(bytes)?)
715}
716
717fn convert_wastargs(args: Vec<wast::WastArg>) -> Result<Vec<WasmValue>> {
718 args.into_iter().map(wastarg2tinywasmvalue).collect()
719}
720
721fn convert_wastret<'a>(args: impl Iterator<Item = wast::WastRet<'a>>) -> Result<Vec<Vec<WasmValue>>> {
722 let mut alternatives = vec![Vec::new()];
723 for arg in args {
724 let choices = wastret2tinywasmvalues(arg)?;
725 let mut next = Vec::with_capacity(alternatives.len() * choices.len());
726 for prefix in alternatives {
727 for choice in &choices {
728 let mut candidate = prefix.clone();
729 candidate.push(*choice);
730 next.push(candidate);
731 }
732 }
733 alternatives = next;
734 }
735 Ok(alternatives)
736}
737
738fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result<WasmValue> {
739 let wast::WastArg::Core(arg) = arg else { bail!("unsupported arg type: Component") };
740 use wast::core::WastArgCore::*;
741 Ok(match arg {
742 F32(f) => WasmValue::F32(f32::from_bits(f.bits)),
743 F64(f) => WasmValue::F64(f64::from_bits(f.bits)),
744 I32(i) => WasmValue::I32(i),
745 I64(i) => WasmValue::I64(i),
746 V128(i) => WasmValue::V128(i.to_le_bytes()),
747 RefExtern(v) => WasmValue::RefExtern(ExternRef::new(Some(v))),
748 RefNull(t) => match t {
749 wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Func } => {
750 WasmValue::RefFunc(FuncRef::null())
751 }
752 wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Extern } => {
753 WasmValue::RefExtern(ExternRef::null())
754 }
755 _ => bail!("unsupported arg type: refnull: {:?}", t),
756 },
757 RefHost(_) => bail!("unsupported arg type: RefHost"),
758 })
759}
760
761fn wast_v128_to_bytes(i: wast::core::V128Pattern) -> [u8; 16] {
762 let res: Vec<u8> = match i {
763 wast::core::V128Pattern::F32x4(f) => {
764 f.iter().flat_map(|v| nanpattern2tinywasmvalue(*v).unwrap().as_f32().unwrap().to_le_bytes()).collect()
765 }
766 wast::core::V128Pattern::F64x2(f) => {
767 f.iter().flat_map(|v| nanpattern2tinywasmvalue(*v).unwrap().as_f64().unwrap().to_le_bytes()).collect()
768 }
769 wast::core::V128Pattern::I16x8(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(),
770 wast::core::V128Pattern::I32x4(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(),
771 wast::core::V128Pattern::I64x2(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(),
772 wast::core::V128Pattern::I8x16(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(),
773 };
774 res.try_into().unwrap()
775}
776
777fn wastret2tinywasmvalues(ret: wast::WastRet) -> Result<Vec<WasmValue>> {
778 let wast::WastRet::Core(ret) = ret else { bail!("unsupported arg type") };
779 match ret {
780 wast::core::WastRetCore::Either(options) => {
781 options.into_iter().map(wastretcore2tinywasmvalue).collect::<Result<Vec<_>>>()
782 }
783 ret => Ok(vec![wastretcore2tinywasmvalue(ret)?]),
784 }
785}
786
787fn wastretcore2tinywasmvalue(ret: wast::core::WastRetCore) -> Result<WasmValue> {
788 use wast::core::WastRetCore::{F32, F64, I32, I64, RefExtern, RefFunc, RefNull, V128};
789 Ok(match ret {
790 F32(f) => nanpattern2tinywasmvalue(f)?,
791 F64(f) => nanpattern2tinywasmvalue(f)?,
792 I32(i) => WasmValue::I32(i),
793 I64(i) => WasmValue::I64(i),
794 V128(i) => WasmValue::V128(wast_v128_to_bytes(i)),
795 RefNull(t) => match t {
796 Some(wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Func }) => {
797 WasmValue::RefFunc(FuncRef::null())
798 }
799 Some(wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Extern }) => {
800 WasmValue::RefExtern(ExternRef::null())
801 }
802 _ => bail!("unsupported arg type: refnull: {:?}", t),
803 },
804 RefExtern(v) => WasmValue::RefExtern(ExternRef::new(v)),
805 RefFunc(v) => WasmValue::RefFunc(FuncRef::new(match v {
806 Some(wast::token::Index::Num(n, _)) => Some(n),
807 _ => bail!("unsupported arg type: reffunc: {:?}", v),
808 })),
809 a => bail!("unsupported arg type {:?}", a),
810 })
811}
812
813enum Bits {
814 U32(u32),
815 U64(u64),
816}
817
818trait FloatToken {
819 fn bits(&self) -> Bits;
820 fn canonical_nan() -> WasmValue;
821 fn arithmetic_nan() -> WasmValue;
822 fn value(&self) -> WasmValue {
823 match self.bits() {
824 Bits::U32(v) => WasmValue::F32(f32::from_bits(v)),
825 Bits::U64(v) => WasmValue::F64(f64::from_bits(v)),
826 }
827 }
828}
829
830impl FloatToken for wast::token::F32 {
831 fn bits(&self) -> Bits {
832 Bits::U32(self.bits)
833 }
834
835 fn canonical_nan() -> WasmValue {
836 WasmValue::F32(f32::NAN)
837 }
838
839 fn arithmetic_nan() -> WasmValue {
840 WasmValue::F32(f32::NAN)
841 }
842}
843
844impl FloatToken for wast::token::F64 {
845 fn bits(&self) -> Bits {
846 Bits::U64(self.bits)
847 }
848
849 fn canonical_nan() -> WasmValue {
850 WasmValue::F64(f64::NAN)
851 }
852
853 fn arithmetic_nan() -> WasmValue {
854 WasmValue::F64(f64::NAN)
855 }
856}
857
858fn nanpattern2tinywasmvalue<T>(arg: wast::core::NanPattern<T>) -> Result<WasmValue>
859where
860 T: FloatToken,
861{
862 use wast::core::NanPattern::{ArithmeticNan, CanonicalNan, Value};
863 Ok(match arg {
864 CanonicalNan => T::canonical_nan(),
865 ArithmeticNan => T::arithmetic_nan(),
866 Value(v) => v.value(),
867 })
868}
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873
874 #[test]
875 fn runs_simple_wast_file() {
876 let dir = tempfile::tempdir().unwrap();
877 let path = dir.path().join("simple.wast");
878 std::fs::write(
879 &path,
880 "(module (func (export \"add\") (result i32) i32.const 1))\n(assert_return (invoke \"add\") (i32.const 1))",
881 )
882 .unwrap();
883
884 let mut runner = WastRunner::new();
885 runner.run_paths(&[path]).unwrap();
886 }
887}