Skip to main content

tinywasm_cli/
wast_runner.rs

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}