wasmtime_wast/
wast.rs

1#[cfg(feature = "component-model")]
2use crate::component;
3use crate::core;
4use crate::spectest::*;
5use anyhow::{Context as _, anyhow, bail};
6use json_from_wast::{Action, Command, Const, WasmFile, WasmFileType};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9use std::str;
10use std::sync::Arc;
11use std::thread;
12use wasmtime::*;
13use wast::lexer::Lexer;
14use wast::parser::{self, ParseBuffer};
15
16/// The wast test script language allows modules to be defined and actions
17/// to be performed on them.
18pub struct WastContext {
19    /// Wast files have a concept of a "current" module, which is the most
20    /// recently defined.
21    current: Option<InstanceKind>,
22    core_linker: Linker<()>,
23    modules: HashMap<String, ModuleKind>,
24    #[cfg(feature = "component-model")]
25    component_linker: component::Linker<()>,
26
27    /// The store used for core wasm tests/primitives.
28    ///
29    /// Note that components each get their own store so this is not used for
30    /// component-model testing.
31    pub(crate) core_store: Store<()>,
32    pub(crate) async_runtime: Option<tokio::runtime::Runtime>,
33    generate_dwarf: bool,
34    precompile_save: Option<PathBuf>,
35    precompile_load: Option<PathBuf>,
36
37    modules_by_filename: Arc<HashMap<String, Vec<u8>>>,
38    configure_store: Arc<dyn Fn(&mut Store<()>) + Send + Sync>,
39}
40
41enum Outcome<T = Results> {
42    Ok(T),
43    Trap(Error),
44}
45
46impl<T> Outcome<T> {
47    fn map<U>(self, map: impl FnOnce(T) -> U) -> Outcome<U> {
48        match self {
49            Outcome::Ok(t) => Outcome::Ok(map(t)),
50            Outcome::Trap(t) => Outcome::Trap(t),
51        }
52    }
53
54    fn into_result(self) -> Result<T> {
55        match self {
56            Outcome::Ok(t) => Ok(t),
57            Outcome::Trap(t) => Err(t),
58        }
59    }
60}
61
62#[derive(Debug)]
63enum Results {
64    Core(Vec<Val>),
65    #[cfg(feature = "component-model")]
66    Component(Vec<component::Val>),
67}
68
69#[derive(Clone)]
70enum ModuleKind {
71    Core(Module),
72    #[cfg(feature = "component-model")]
73    Component(component::Component),
74}
75
76enum InstanceKind {
77    Core(Instance),
78    #[cfg(feature = "component-model")]
79    Component(Store<()>, component::Instance),
80}
81
82enum Export<'a> {
83    Core(Extern),
84    #[cfg(feature = "component-model")]
85    Component(&'a mut Store<()>, component::Func),
86
87    /// Impossible-to-construct variant to consider `'a` used when the
88    /// `component-model` feature is disabled.
89    _Unused(std::convert::Infallible, &'a ()),
90}
91
92/// Whether or not to use async APIs when calling wasm during wast testing.
93///
94/// Passed to [`WastContext::new`].
95#[derive(Debug, Copy, Clone, PartialEq)]
96#[expect(missing_docs, reason = "self-describing variants")]
97pub enum Async {
98    Yes,
99    No,
100}
101
102impl WastContext {
103    /// Construct a new instance of `WastContext`.
104    ///
105    /// The `engine` provided is used for all store/module/component creation
106    /// and should be appropriately configured by the caller. The `async_`
107    /// configuration indicates whether functions are invoked either async or
108    /// sync, and then the `configure` callback is used whenever a store is
109    /// created to further configure its settings.
110    pub fn new(
111        engine: &Engine,
112        async_: Async,
113        configure: impl Fn(&mut Store<()>) + Send + Sync + 'static,
114    ) -> Self {
115        // Spec tests will redefine the same module/name sometimes, so we need
116        // to allow shadowing in the linker which picks the most recent
117        // definition as what to link when linking.
118        let mut core_linker = Linker::new(engine);
119        core_linker.allow_shadowing(true);
120        Self {
121            current: None,
122            core_linker,
123            #[cfg(feature = "component-model")]
124            component_linker: {
125                let mut linker = component::Linker::new(engine);
126                linker.allow_shadowing(true);
127                linker
128            },
129            core_store: {
130                let mut store = Store::new(engine, ());
131                configure(&mut store);
132                store
133            },
134            modules: Default::default(),
135            async_runtime: if async_ == Async::Yes {
136                Some(
137                    tokio::runtime::Builder::new_current_thread()
138                        .build()
139                        .unwrap(),
140                )
141            } else {
142                None
143            },
144            generate_dwarf: true,
145            precompile_save: None,
146            precompile_load: None,
147            modules_by_filename: Arc::default(),
148            configure_store: Arc::new(configure),
149        }
150    }
151
152    fn engine(&self) -> &Engine {
153        self.core_linker.engine()
154    }
155
156    /// Saves precompiled modules/components into `path` instead of executing
157    /// test directives.
158    pub fn precompile_save(&mut self, path: impl AsRef<Path>) -> &mut Self {
159        self.precompile_save = Some(path.as_ref().into());
160        self
161    }
162
163    /// Loads precompiled modules/components from `path` instead of compiling
164    /// natively.
165    pub fn precompile_load(&mut self, path: impl AsRef<Path>) -> &mut Self {
166        self.precompile_load = Some(path.as_ref().into());
167        self
168    }
169
170    fn get_export(&mut self, module: Option<&str>, name: &str) -> Result<Export<'_>> {
171        if let Some(module) = module {
172            return Ok(Export::Core(
173                self.core_linker
174                    .get(&mut self.core_store, module, name)
175                    .ok_or_else(|| anyhow!("no item named `{module}::{name}` found"))?,
176            ));
177        }
178
179        let cur = self
180            .current
181            .as_mut()
182            .ok_or_else(|| anyhow!("no previous instance found"))?;
183        Ok(match cur {
184            InstanceKind::Core(i) => Export::Core(
185                i.get_export(&mut self.core_store, name)
186                    .ok_or_else(|| anyhow!("no item named `{name}` found"))?,
187            ),
188            #[cfg(feature = "component-model")]
189            InstanceKind::Component(store, i) => {
190                let export = i
191                    .get_func(&mut *store, name)
192                    .ok_or_else(|| anyhow!("no func named `{name}` found"))?;
193                Export::Component(store, export)
194            }
195        })
196    }
197
198    fn instantiate_module(&mut self, module: &Module) -> Result<Outcome<Instance>> {
199        let instance = match &self.async_runtime {
200            Some(rt) => rt.block_on(
201                self.core_linker
202                    .instantiate_async(&mut self.core_store, &module),
203            ),
204            None => self.core_linker.instantiate(&mut self.core_store, &module),
205        };
206        Ok(match instance {
207            Ok(i) => Outcome::Ok(i),
208            Err(e) => Outcome::Trap(e),
209        })
210    }
211
212    #[cfg(feature = "component-model")]
213    fn instantiate_component(
214        &mut self,
215        component: &component::Component,
216    ) -> Result<Outcome<(component::Component, Store<()>, component::Instance)>> {
217        let mut store = Store::new(self.engine(), ());
218        (self.configure_store)(&mut store);
219        let instance = match &self.async_runtime {
220            Some(rt) => rt.block_on(
221                self.component_linker
222                    .instantiate_async(&mut store, &component),
223            ),
224            None => self.component_linker.instantiate(&mut store, &component),
225        };
226        Ok(match instance {
227            Ok(i) => Outcome::Ok((component.clone(), store, i)),
228            Err(e) => Outcome::Trap(e),
229        })
230    }
231
232    /// Register "spectest" which is used by the spec testsuite.
233    pub fn register_spectest(&mut self, config: &SpectestConfig) -> Result<()> {
234        link_spectest(&mut self.core_linker, &mut self.core_store, config)?;
235        #[cfg(feature = "component-model")]
236        link_component_spectest(&mut self.component_linker)?;
237        Ok(())
238    }
239
240    /// Perform the action portion of a command.
241    fn perform_action(&mut self, action: &Action<'_>) -> Result<Outcome> {
242        // Need to simultaneously borrow `self.async_runtime` and a `&mut
243        // Store` from components so work around the borrow checker issues by
244        // taking out the async runtime here and putting it back through a
245        // destructor.
246        struct ReplaceRuntime<'a> {
247            ctx: &'a mut WastContext,
248            rt: Option<tokio::runtime::Runtime>,
249        }
250        impl Drop for ReplaceRuntime<'_> {
251            fn drop(&mut self) {
252                self.ctx.async_runtime = self.rt.take();
253            }
254        }
255        let replace = ReplaceRuntime {
256            rt: self.async_runtime.take(),
257            ctx: self,
258        };
259        let me = &mut *replace.ctx;
260        match action {
261            Action::Invoke {
262                module,
263                field,
264                args,
265            } => match me.get_export(module.as_deref(), field)? {
266                Export::Core(export) => {
267                    drop(replace);
268                    let func = export
269                        .into_func()
270                        .ok_or_else(|| anyhow!("no function named `{field}`"))?;
271                    let values = args
272                        .iter()
273                        .map(|v| match v {
274                            Const::Core(v) => core::val(self, v),
275                            _ => bail!("expected core function, found other other argument {v:?}"),
276                        })
277                        .collect::<Result<Vec<_>>>()?;
278
279                    let mut results =
280                        vec![Val::null_func_ref(); func.ty(&self.core_store).results().len()];
281                    let result = match &self.async_runtime {
282                        Some(rt) => rt.block_on(func.call_async(
283                            &mut self.core_store,
284                            &values,
285                            &mut results,
286                        )),
287                        None => func.call(&mut self.core_store, &values, &mut results),
288                    };
289
290                    Ok(match result {
291                        Ok(()) => Outcome::Ok(Results::Core(results)),
292                        Err(e) => Outcome::Trap(e),
293                    })
294                }
295                #[cfg(feature = "component-model")]
296                Export::Component(store, func) => {
297                    let values = args
298                        .iter()
299                        .map(|v| match v {
300                            Const::Component(v) => component::val(v),
301                            _ => bail!("expected component function, found other argument {v:?}"),
302                        })
303                        .collect::<Result<Vec<_>>>()?;
304
305                    let mut results =
306                        vec![component::Val::Bool(false); func.ty(&store).results().len()];
307                    let result = match &replace.rt {
308                        Some(rt) => {
309                            rt.block_on(func.call_async(&mut *store, &values, &mut results))
310                        }
311                        None => func.call(&mut *store, &values, &mut results),
312                    };
313                    Ok(match result {
314                        Ok(()) => {
315                            let result = match &replace.rt {
316                                Some(rt) => rt.block_on(func.post_return_async(&mut *store)),
317                                None => func.post_return(&mut *store),
318                            };
319
320                            match result {
321                                Ok(()) => Outcome::Ok(Results::Component(results)),
322                                Err(e) => Outcome::Trap(e),
323                            }
324                        }
325                        Err(e) => Outcome::Trap(e),
326                    })
327                }
328            },
329            Action::Get { module, field, .. } => me.get(module.as_deref(), field),
330        }
331    }
332
333    /// Instantiates the `module` provided and registers the instance under the
334    /// `name` provided if successful.
335    fn module(&mut self, name: Option<&str>, module: &ModuleKind) -> Result<()> {
336        match module {
337            ModuleKind::Core(module) => {
338                let instance = match self.instantiate_module(&module)? {
339                    Outcome::Ok(i) => i,
340                    Outcome::Trap(e) => return Err(e).context("instantiation failed"),
341                };
342                if let Some(name) = name {
343                    self.core_linker
344                        .instance(&mut self.core_store, name, instance)?;
345                }
346                self.current = Some(InstanceKind::Core(instance));
347            }
348            #[cfg(feature = "component-model")]
349            ModuleKind::Component(module) => {
350                let (component, mut store, instance) = match self.instantiate_component(&module)? {
351                    Outcome::Ok(i) => i,
352                    Outcome::Trap(e) => return Err(e).context("instantiation failed"),
353                };
354                if let Some(name) = name {
355                    let ty = component.component_type();
356                    let engine = self.engine().clone();
357                    let mut linker = self.component_linker.instance(name)?;
358                    for (name, item) in ty.exports(&engine) {
359                        match item {
360                            component::types::ComponentItem::Module(_) => {
361                                let module = instance.get_module(&mut store, name).unwrap();
362                                linker.module(name, &module)?;
363                            }
364                            component::types::ComponentItem::Resource(_) => {
365                                let resource = instance.get_resource(&mut store, name).unwrap();
366                                linker.resource(name, resource, |_, _| Ok(()))?;
367                            }
368                            // TODO: should ideally reflect more than just
369                            // modules/resources into the linker's namespace
370                            // but that's not easily supported today for host
371                            // functions due to the inability to take a
372                            // function from one instance and put it into the
373                            // linker (must go through the host right now).
374                            _ => {}
375                        }
376                    }
377                }
378                self.current = Some(InstanceKind::Component(store, instance));
379            }
380        }
381        Ok(())
382    }
383
384    /// Compiles the module `wat` into binary and returns the name found within
385    /// it, if any.
386    ///
387    /// This will not register the name within `self.modules`.
388    fn module_definition(&mut self, file: &WasmFile) -> Result<ModuleKind> {
389        let name = match file.module_type {
390            WasmFileType::Text => file
391                .binary_filename
392                .as_ref()
393                .ok_or_else(|| anyhow!("cannot compile module that isn't a valid binary"))?,
394            WasmFileType::Binary => &file.filename,
395        };
396
397        match &self.precompile_load {
398            Some(path) => {
399                let cwasm = path.join(&name[..]).with_extension("cwasm");
400                match Engine::detect_precompiled_file(&cwasm)
401                    .with_context(|| format!("failed to read {cwasm:?}"))?
402                {
403                    Some(Precompiled::Module) => {
404                        let module = unsafe { Module::deserialize_file(self.engine(), &cwasm)? };
405                        Ok(ModuleKind::Core(module))
406                    }
407                    #[cfg(feature = "component-model")]
408                    Some(Precompiled::Component) => {
409                        let component = unsafe {
410                            component::Component::deserialize_file(self.engine(), &cwasm)?
411                        };
412                        Ok(ModuleKind::Component(component))
413                    }
414                    #[cfg(not(feature = "component-model"))]
415                    Some(Precompiled::Component) => {
416                        bail!("support for components disabled at compile time")
417                    }
418                    None => bail!("expected a cwasm file"),
419                }
420            }
421            None => {
422                let bytes = &self.modules_by_filename[&name[..]];
423
424                if wasmparser::Parser::is_core_wasm(&bytes) {
425                    let module = Module::new(self.engine(), &bytes)?;
426                    Ok(ModuleKind::Core(module))
427                } else {
428                    #[cfg(feature = "component-model")]
429                    {
430                        let component = component::Component::new(self.engine(), &bytes)?;
431                        Ok(ModuleKind::Component(component))
432                    }
433                    #[cfg(not(feature = "component-model"))]
434                    bail!("component-model support not enabled");
435                }
436            }
437        }
438    }
439
440    /// Register an instance to make it available for performing actions.
441    fn register(&mut self, name: Option<&str>, as_name: &str) -> Result<()> {
442        match name {
443            Some(name) => self.core_linker.alias_module(name, as_name),
444            None => {
445                let current = self
446                    .current
447                    .as_ref()
448                    .ok_or(anyhow!("no previous instance"))?;
449                match current {
450                    InstanceKind::Core(current) => {
451                        self.core_linker
452                            .instance(&mut self.core_store, as_name, *current)?;
453                    }
454                    #[cfg(feature = "component-model")]
455                    InstanceKind::Component(..) => {
456                        bail!("register not implemented for components");
457                    }
458                }
459                Ok(())
460            }
461        }
462    }
463
464    /// Get the value of an exported global from an instance.
465    fn get(&mut self, instance_name: Option<&str>, field: &str) -> Result<Outcome> {
466        let global = match self.get_export(instance_name, field)? {
467            Export::Core(e) => e
468                .into_global()
469                .ok_or_else(|| anyhow!("no global named `{field}`"))?,
470            #[cfg(feature = "component-model")]
471            Export::Component(..) => bail!("no global named `{field}`"),
472        };
473        Ok(Outcome::Ok(Results::Core(vec![
474            global.get(&mut self.core_store),
475        ])))
476    }
477
478    fn assert_return(&mut self, result: Outcome, results: &[Const]) -> Result<()> {
479        match result.into_result()? {
480            Results::Core(values) => {
481                if values.len() != results.len() {
482                    bail!("expected {} results found {}", results.len(), values.len());
483                }
484                for (i, (v, e)) in values.iter().zip(results).enumerate() {
485                    let e = match e {
486                        Const::Core(core) => core,
487                        _ => bail!("expected core value found other value {e:?}"),
488                    };
489                    core::match_val(&mut self.core_store, v, e)
490                        .with_context(|| format!("result {i} didn't match"))?;
491                }
492            }
493            #[cfg(feature = "component-model")]
494            Results::Component(values) => {
495                if values.len() != results.len() {
496                    bail!("expected {} results found {}", results.len(), values.len());
497                }
498                for (i, (v, e)) in values.iter().zip(results).enumerate() {
499                    let e = match e {
500                        Const::Component(val) => val,
501                        _ => bail!("expected component value found other value {e:?}"),
502                    };
503                    component::match_val(e, v)
504                        .with_context(|| format!("result {i} didn't match"))?;
505                }
506            }
507        }
508        Ok(())
509    }
510
511    fn assert_trap(&self, result: Outcome, expected: &str) -> Result<()> {
512        let trap = match result {
513            Outcome::Ok(values) => bail!("expected trap, got {values:?}"),
514            Outcome::Trap(t) => t,
515        };
516        let actual = format!("{trap:?}");
517        if actual.contains(expected)
518            // `bulk-memory-operations/bulk.wast` checks for a message that
519            // specifies which element is uninitialized, but our traps don't
520            // shepherd that information out.
521            || (expected.contains("uninitialized element 2") && actual.contains("uninitialized element"))
522            // function references call_ref
523            || (expected.contains("null function") && (actual.contains("uninitialized element") || actual.contains("null reference")))
524            // GC tests say "null $kind reference" but we just say "null reference".
525            || (expected.contains("null") && expected.contains("reference") && actual.contains("null reference"))
526        {
527            return Ok(());
528        }
529        bail!("expected '{expected}', got '{actual}'")
530    }
531
532    fn assert_exception(&mut self, result: Outcome) -> Result<()> {
533        match result {
534            Outcome::Ok(values) => bail!("expected exception, got {values:?}"),
535            Outcome::Trap(err) if err.is::<ThrownException>() => {
536                // Discard the thrown exception.
537                let _ = self
538                    .core_store
539                    .take_pending_exception()
540                    .expect("there should be a pending exception on the store");
541                Ok(())
542            }
543            Outcome::Trap(err) => bail!("expected exception, got {err:?}"),
544        }
545    }
546
547    /// Run a wast script from a byte buffer.
548    pub fn run_wast(&mut self, filename: &str, wast: &[u8]) -> Result<()> {
549        let wast = str::from_utf8(wast)?;
550
551        let adjust_wast = |mut err: wast::Error| {
552            err.set_path(filename.as_ref());
553            err.set_text(wast);
554            err
555        };
556
557        let mut lexer = Lexer::new(wast);
558        lexer.allow_confusing_unicode(filename.ends_with("names.wast"));
559        let mut buf = ParseBuffer::new_with_lexer(lexer).map_err(adjust_wast)?;
560        buf.track_instr_spans(self.generate_dwarf);
561        let ast = parser::parse::<wast::Wast>(&buf).map_err(adjust_wast)?;
562
563        let mut ast = json_from_wast::Opts::default()
564            .dwarf(self.generate_dwarf)
565            .convert(filename, wast, ast)?;
566        let modules_by_filename = Arc::get_mut(&mut self.modules_by_filename).unwrap();
567        for (name, bytes) in ast.wasms.drain(..) {
568            let prev = modules_by_filename.insert(name, bytes);
569            assert!(prev.is_none());
570        }
571
572        match &self.precompile_save {
573            Some(path) => {
574                let json_path = path
575                    .join(Path::new(filename).file_name().unwrap())
576                    .with_extension("json");
577                let json = serde_json::to_string(&ast)?;
578                std::fs::write(&json_path, json)
579                    .with_context(|| format!("failed to write {json_path:?}"))?;
580                for (name, bytes) in self.modules_by_filename.iter() {
581                    let cwasm_path = path.join(name).with_extension("cwasm");
582                    let cwasm = if wasmparser::Parser::is_core_wasm(&bytes) {
583                        self.engine().precompile_module(bytes)
584                    } else {
585                        #[cfg(feature = "component-model")]
586                        {
587                            self.engine().precompile_component(bytes)
588                        }
589                        #[cfg(not(feature = "component-model"))]
590                        bail!("component-model support not enabled");
591                    };
592                    if let Ok(cwasm) = cwasm {
593                        std::fs::write(&cwasm_path, cwasm)
594                            .with_context(|| format!("failed to write {cwasm_path:?}"))?;
595                    }
596                }
597                Ok(())
598            }
599            None => self.run_directives(ast.commands, filename),
600        }
601    }
602
603    fn run_directives(&mut self, directives: Vec<Command<'_>>, filename: &str) -> Result<()> {
604        thread::scope(|scope| {
605            let mut threads = HashMap::new();
606            for directive in directives {
607                let line = directive.line();
608                log::debug!("running directive on {filename}:{line}");
609                self.run_directive(directive, filename, &scope, &mut threads)
610                    .with_context(|| format!("failed directive on {filename}:{line}"))?;
611            }
612            Ok(())
613        })
614    }
615
616    fn run_directive<'a>(
617        &mut self,
618        directive: Command<'a>,
619        filename: &'a str,
620        // wast: &'a str,
621        scope: &'a thread::Scope<'a, '_>,
622        threads: &mut HashMap<String, thread::ScopedJoinHandle<'a, Result<()>>>,
623    ) -> Result<()> {
624        use Command::*;
625
626        match directive {
627            Module {
628                name,
629                file,
630                line: _,
631            } => {
632                let module = self.module_definition(&file)?;
633                self.module(name.as_deref(), &module)?;
634            }
635            ModuleDefinition {
636                name,
637                file,
638                line: _,
639            } => {
640                let module = self.module_definition(&file)?;
641                if let Some(name) = name {
642                    self.modules.insert(name.to_string(), module);
643                }
644            }
645            ModuleInstance {
646                instance,
647                module,
648                line: _,
649            } => {
650                let module = module
651                    .as_deref()
652                    .and_then(|n| self.modules.get(n))
653                    .cloned()
654                    .ok_or_else(|| anyhow!("no module named {module:?}"))?;
655                self.module(instance.as_deref(), &module)?;
656            }
657            Register { line: _, name, as_ } => {
658                self.register(name.as_deref(), &as_)?;
659            }
660            Action { action, line: _ } => {
661                self.perform_action(&action)?;
662            }
663            AssertReturn {
664                action,
665                expected,
666                line: _,
667            } => {
668                let result = self.perform_action(&action)?;
669                self.assert_return(result, &expected)?;
670            }
671            AssertTrap {
672                action,
673                text,
674                line: _,
675            } => {
676                let result = self.perform_action(&action)?;
677                self.assert_trap(result, &text)?;
678            }
679            AssertUninstantiable {
680                file,
681                text,
682                line: _,
683            } => {
684                let result = match self.module_definition(&file)? {
685                    ModuleKind::Core(module) => self
686                        .instantiate_module(&module)?
687                        .map(|_| Results::Core(Vec::new())),
688                    #[cfg(feature = "component-model")]
689                    ModuleKind::Component(component) => self
690                        .instantiate_component(&component)?
691                        .map(|_| Results::Component(Vec::new())),
692                };
693                self.assert_trap(result, &text)?;
694            }
695            AssertExhaustion {
696                action,
697                text,
698                line: _,
699            } => {
700                let result = self.perform_action(&action)?;
701                self.assert_trap(result, &text)?;
702            }
703            AssertInvalid {
704                file,
705                text,
706                line: _,
707            } => {
708                let err = match self.module_definition(&file) {
709                    Ok(_) => bail!("expected module to fail to build"),
710                    Err(e) => e,
711                };
712                let error_message = format!("{err:?}");
713                if !is_matching_assert_invalid_error_message(filename, &text, &error_message) {
714                    bail!("assert_invalid: expected \"{text}\", got \"{error_message}\"",)
715                }
716            }
717            AssertMalformed {
718                file,
719                text: _,
720                line: _,
721            } => {
722                if let Ok(_) = self.module_definition(&file) {
723                    bail!("expected malformed module to fail to instantiate");
724                }
725            }
726            AssertUnlinkable {
727                file,
728                text,
729                line: _,
730            } => {
731                let module = self.module_definition(&file)?;
732                let err = match self.module(None, &module) {
733                    Ok(_) => bail!("expected module to fail to link"),
734                    Err(e) => e,
735                };
736                let error_message = format!("{err:?}");
737                if !is_matching_assert_invalid_error_message(filename, &text, &error_message) {
738                    bail!("assert_unlinkable: expected {text}, got {error_message}",)
739                }
740            }
741            AssertException { line: _, action } => {
742                let result = self.perform_action(&action)?;
743                self.assert_exception(result)?;
744            }
745
746            Thread {
747                name,
748                shared_module,
749                commands,
750                line: _,
751            } => {
752                let mut core_linker = Linker::new(self.engine());
753                if let Some(id) = shared_module {
754                    let items = self
755                        .core_linker
756                        .iter(&mut self.core_store)
757                        .filter(|(module, _, _)| *module == &id[..])
758                        .collect::<Vec<_>>();
759                    for (module, name, item) in items {
760                        core_linker.define(&mut self.core_store, module, name, item)?;
761                    }
762                }
763                let mut child_cx = WastContext {
764                    current: None,
765                    core_linker,
766                    #[cfg(feature = "component-model")]
767                    component_linker: component::Linker::new(self.engine()),
768                    core_store: {
769                        let mut store = Store::new(self.engine(), ());
770                        (self.configure_store)(&mut store);
771                        store
772                    },
773                    modules: self.modules.clone(),
774                    async_runtime: self.async_runtime.as_ref().map(|_| {
775                        tokio::runtime::Builder::new_current_thread()
776                            .build()
777                            .unwrap()
778                    }),
779                    generate_dwarf: self.generate_dwarf,
780                    modules_by_filename: self.modules_by_filename.clone(),
781                    precompile_load: self.precompile_load.clone(),
782                    precompile_save: self.precompile_save.clone(),
783                    configure_store: self.configure_store.clone(),
784                };
785                let child = scope.spawn(move || child_cx.run_directives(commands, filename));
786                threads.insert(name.to_string(), child);
787            }
788            Wait { thread, .. } => {
789                threads
790                    .remove(&thread[..])
791                    .ok_or_else(|| anyhow!("no thread named `{thread}`"))?
792                    .join()
793                    .unwrap()?;
794            }
795
796            AssertSuspension { .. } => {
797                bail!("unimplemented wast directive");
798            }
799        }
800
801        Ok(())
802    }
803
804    /// Run a wast script from a file.
805    pub fn run_file(&mut self, path: &Path) -> Result<()> {
806        match &self.precompile_load {
807            Some(precompile) => {
808                let file = precompile
809                    .join(path.file_name().unwrap())
810                    .with_extension("json");
811                let json = std::fs::read_to_string(&file)
812                    .with_context(|| format!("failed to read {file:?}"))?;
813                let wast = serde_json::from_str::<json_from_wast::Wast<'_>>(&json)?;
814                self.run_directives(wast.commands, &wast.source_filename)
815            }
816            None => {
817                let bytes = std::fs::read(path)
818                    .with_context(|| format!("failed to read `{}`", path.display()))?;
819                self.run_wast(path.to_str().unwrap(), &bytes)
820            }
821        }
822    }
823
824    /// Whether or not to generate DWARF debugging information in custom
825    /// sections in modules being tested.
826    pub fn generate_dwarf(&mut self, enable: bool) -> &mut Self {
827        self.generate_dwarf = enable;
828        self
829    }
830}
831
832fn is_matching_assert_invalid_error_message(test: &str, expected: &str, actual: &str) -> bool {
833    if actual.contains(expected) {
834        return true;
835    }
836
837    // Historically wasmtime/wasm-tools tried to match the upstream error
838    // message. This generally led to a large sequence of matches here which is
839    // not easy to maintain and is particularly difficult when test suites and
840    // proposals conflict with each other (e.g. one asserts one error message
841    // and another asserts a different error message). Overall we didn't benefit
842    // a whole lot from trying to match errors so just assume the error is
843    // roughly the same and otherwise don't try to match it.
844    if test.contains("spec_testsuite") {
845        return true;
846    }
847
848    // we are in control over all non-spec tests so all the error messages
849    // there should exactly match the `assert_invalid` or such
850    false
851}