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