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::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}