wasmtime_wast_util/
lib.rs

1use anyhow::{Context, Result};
2use serde::de::DeserializeOwned;
3use serde_derive::Deserialize;
4use std::fmt;
5use std::fs;
6use std::path::Path;
7use std::path::PathBuf;
8
9/// Limits for running wast tests.
10///
11/// This is useful for sharing between `tests/wast.rs` and fuzzing, for
12/// example, and is used as the minimum threshold for configuration when
13/// fuzzing.
14///
15/// Note that it's ok to increase these numbers if a test comes along and needs
16/// it, they're just here as empirically found minimum thresholds so far and
17/// they're not too scientific.
18pub mod limits {
19    pub const MEMORY_SIZE: usize = 805 << 16;
20    pub const MEMORIES: u32 = 450;
21    pub const TABLES: u32 = 200;
22    pub const MEMORIES_PER_MODULE: u32 = 9;
23    pub const TABLES_PER_MODULE: u32 = 5;
24    pub const COMPONENT_INSTANCES: u32 = 50;
25    pub const CORE_INSTANCES: u32 = 900;
26    pub const TABLE_ELEMENTS: usize = 1000;
27    pub const CORE_INSTANCE_SIZE: usize = 64 * 1024;
28}
29
30/// Local all `*.wast` tests under `root` which should be the path to the root
31/// of the wasmtime repository.
32pub fn find_tests(root: &Path) -> Result<Vec<WastTest>> {
33    let mut tests = Vec::new();
34    add_tests(&mut tests, &root.join("tests/spec_testsuite"), false)?;
35    add_tests(&mut tests, &root.join("tests/misc_testsuite"), true)?;
36    Ok(tests)
37}
38
39fn add_tests(tests: &mut Vec<WastTest>, path: &Path, has_config: bool) -> Result<()> {
40    for entry in path.read_dir().context("failed to read directory")? {
41        let entry = entry.context("failed to read directory entry")?;
42        let path = entry.path();
43        if entry
44            .file_type()
45            .context("failed to get file type")?
46            .is_dir()
47        {
48            add_tests(tests, &path, has_config).context("failed to read sub-directory")?;
49            continue;
50        }
51
52        if path.extension().and_then(|s| s.to_str()) != Some("wast") {
53            continue;
54        }
55
56        let contents =
57            fs::read_to_string(&path).with_context(|| format!("failed to read test: {path:?}"))?;
58        let config = if has_config {
59            parse_test_config(&contents)
60                .with_context(|| format!("failed to parse test configuration: {path:?}"))?
61        } else {
62            spec_test_config(&path)
63        };
64        tests.push(WastTest {
65            path,
66            contents,
67            config,
68        })
69    }
70    Ok(())
71}
72
73fn spec_test_config(test: &Path) -> TestConfig {
74    let mut ret = TestConfig::default();
75    match spec_proposal_from_path(test) {
76        Some("multi-memory") => {
77            ret.multi_memory = Some(true);
78            ret.reference_types = Some(true);
79            ret.simd = Some(true);
80        }
81        Some("wide-arithmetic") => {
82            ret.wide_arithmetic = Some(true);
83        }
84        Some("threads") => {
85            ret.threads = Some(true);
86            ret.reference_types = Some(false);
87        }
88        Some("tail-call") => {
89            ret.tail_call = Some(true);
90            ret.reference_types = Some(true);
91        }
92        Some("relaxed-simd") => {
93            ret.relaxed_simd = Some(true);
94        }
95        Some("memory64") => {
96            ret.memory64 = Some(true);
97            ret.tail_call = Some(true);
98            ret.gc = Some(true);
99            ret.extended_const = Some(true);
100            ret.multi_memory = Some(true);
101            ret.relaxed_simd = Some(true);
102        }
103        Some("extended-const") => {
104            ret.extended_const = Some(true);
105            ret.reference_types = Some(true);
106        }
107        Some("custom-page-sizes") => {
108            ret.custom_page_sizes = Some(true);
109            ret.multi_memory = Some(true);
110        }
111        Some("exception-handling") => {
112            ret.reference_types = Some(true);
113        }
114        Some("gc") => {
115            ret.gc = Some(true);
116            ret.tail_call = Some(true);
117        }
118        Some("function-references") => {
119            ret.function_references = Some(true);
120            ret.tail_call = Some(true);
121        }
122        Some("annotations") => {
123            ret.simd = Some(true);
124        }
125        Some(proposal) => panic!("unsuported proposal {proposal:?}"),
126        None => {
127            ret.reference_types = Some(true);
128            ret.simd = Some(true);
129        }
130    }
131
132    ret
133}
134
135/// Parse test configuration from the specified test, comments starting with
136/// `;;!`.
137pub fn parse_test_config<T>(wat: &str) -> Result<T>
138where
139    T: DeserializeOwned,
140{
141    // The test config source is the leading lines of the WAT file that are
142    // prefixed with `;;!`.
143    let config_lines: Vec<_> = wat
144        .lines()
145        .take_while(|l| l.starts_with(";;!"))
146        .map(|l| &l[3..])
147        .collect();
148    let config_text = config_lines.join("\n");
149
150    toml::from_str(&config_text).context("failed to parse the test configuration")
151}
152
153/// A `*.wast` test with its path, contents, and configuration.
154#[derive(Clone)]
155pub struct WastTest {
156    pub path: PathBuf,
157    pub contents: String,
158    pub config: TestConfig,
159}
160
161impl fmt::Debug for WastTest {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        f.debug_struct("WastTest")
164            .field("path", &self.path)
165            .field("contents", &"...")
166            .field("config", &self.config)
167            .finish()
168    }
169}
170
171macro_rules! foreach_config_option {
172    ($m:ident) => {
173        $m! {
174            memory64
175            custom_page_sizes
176            multi_memory
177            threads
178            gc
179            function_references
180            relaxed_simd
181            reference_types
182            tail_call
183            extended_const
184            wide_arithmetic
185            hogs_memory
186            nan_canonicalization
187            component_model_async
188            simd
189            gc_types
190        }
191    };
192}
193
194macro_rules! define_test_config {
195    ($($option:ident)*) => {
196        /// Per-test configuration which is written down in the test file itself for
197        /// `misc_testsuite/**/*.wast` or in `spec_test_config` above for spec tests.
198        #[derive(Debug, PartialEq, Default, Deserialize, Clone)]
199        #[serde(deny_unknown_fields)]
200        pub struct TestConfig {
201            $(pub $option: Option<bool>,)*
202        }
203
204        impl TestConfig {
205            $(
206                pub fn $option(&self) -> bool {
207                    self.$option.unwrap_or(false)
208                }
209            )*
210        }
211
212    }
213}
214
215foreach_config_option!(define_test_config);
216
217impl TestConfig {
218    /// Returns an iterator over each option.
219    pub fn options_mut(&mut self) -> impl Iterator<Item = (&'static str, &mut Option<bool>)> {
220        macro_rules! mk {
221            ($($option:ident)*) => {
222                [
223                    $((stringify!($option), &mut self.$option),)*
224                ].into_iter()
225            }
226        }
227        foreach_config_option!(mk)
228    }
229}
230
231/// Configuration that spec tests can run under.
232#[derive(Debug)]
233pub struct WastConfig {
234    /// Compiler chosen to run this test.
235    pub compiler: Compiler,
236    /// Whether or not the pooling allocator is enabled.
237    pub pooling: bool,
238    /// What garbage collector is being used.
239    pub collector: Collector,
240}
241
242/// Different compilers that can be tested in Wasmtime.
243#[derive(PartialEq, Debug, Copy, Clone)]
244pub enum Compiler {
245    /// Cranelift backend.
246    ///
247    /// This tests the Cranelift code generator for native platforms. This
248    /// notably excludes Pulley since that's listed separately below even though
249    /// Pulley is a backend of Cranelift. This is only used for native code
250    /// generation such as x86_64.
251    CraneliftNative,
252
253    /// Winch backend.
254    ///
255    /// This tests the Winch backend for native platforms. Currently Winch
256    /// primarily supports x86_64.
257    Winch,
258
259    /// Pulley interpreter.
260    ///
261    /// This tests the Cranelift pulley backend plus the pulley execution
262    /// environment of the output bytecode. Note that this is separate from
263    /// `Cranelift` above to be able to test both on platforms where Cranelift
264    /// has native codegen support.
265    CraneliftPulley,
266}
267
268impl Compiler {
269    /// Returns whether this compiler is known to fail for the provided
270    /// `TestConfig`.
271    ///
272    /// This function will determine if the configuration of the test provided
273    /// is known to guarantee fail. This effectively tracks the proposal support
274    /// for each compiler backend/runtime and tests whether `config` enables or
275    /// disables features that aren't supported.
276    ///
277    /// Note that this is closely aligned with
278    /// `Config::compiler_panicking_wasm_features`.
279    pub fn should_fail(&self, config: &TestConfig) -> bool {
280        match self {
281            // Currently Cranelift supports all wasm proposals that wasmtime
282            // tests.
283            Compiler::CraneliftNative => {}
284
285            // Winch doesn't have quite the full breadth of support that
286            // Cranelift has quite yet.
287            Compiler::Winch => {
288                if config.gc()
289                    || config.tail_call()
290                    || config.function_references()
291                    || config.gc()
292                    || config.relaxed_simd()
293                    || config.gc_types()
294                {
295                    return true;
296                }
297            }
298
299            Compiler::CraneliftPulley => {
300                // Pulley at this time fundamentally does not support threads
301                // due to being unable to implement non-atomic loads/stores
302                // safely.
303                if config.threads() {
304                    return true;
305                }
306            }
307        }
308
309        false
310    }
311
312    /// Returns whether this compiler configuration supports the current host
313    /// architecture.
314    pub fn supports_host(&self) -> bool {
315        match self {
316            Compiler::CraneliftNative => {
317                cfg!(target_arch = "x86_64")
318                    || cfg!(target_arch = "aarch64")
319                    || cfg!(target_arch = "riscv64")
320                    || cfg!(target_arch = "s390x")
321            }
322            Compiler::Winch => {
323                cfg!(target_arch = "x86_64")
324            }
325            Compiler::CraneliftPulley => true,
326        }
327    }
328}
329
330#[derive(PartialEq, Debug, Copy, Clone)]
331pub enum Collector {
332    Auto,
333    Null,
334    DeferredReferenceCounting,
335}
336
337impl WastTest {
338    /// Returns whether this test exercises the GC types and might want to use
339    /// multiple different garbage collectors.
340    pub fn test_uses_gc_types(&self) -> bool {
341        self.config.gc() || self.config.function_references()
342    }
343
344    /// Returns the optional spec proposal that this test is associated with.
345    pub fn spec_proposal(&self) -> Option<&str> {
346        spec_proposal_from_path(&self.path)
347    }
348
349    /// Returns whether this test should fail under the specified extra
350    /// configuration.
351    pub fn should_fail(&self, config: &WastConfig) -> bool {
352        if !config.compiler.supports_host() {
353            return true;
354        }
355
356        // Some tests are known to fail with the pooling allocator
357        if config.pooling {
358            let unsupported = [
359                // allocates too much memory for the pooling configuration here
360                "misc_testsuite/memory64/more-than-4gb.wast",
361                // shared memories + pooling allocator aren't supported yet
362                "misc_testsuite/memory-combos.wast",
363                "misc_testsuite/threads/LB.wast",
364                "misc_testsuite/threads/LB_atomic.wast",
365                "misc_testsuite/threads/MP.wast",
366                "misc_testsuite/threads/MP_atomic.wast",
367                "misc_testsuite/threads/MP_wait.wast",
368                "misc_testsuite/threads/SB.wast",
369                "misc_testsuite/threads/SB_atomic.wast",
370                "misc_testsuite/threads/atomics_notify.wast",
371                "misc_testsuite/threads/atomics_wait_address.wast",
372                "misc_testsuite/threads/wait_notify.wast",
373                "spec_testsuite/proposals/threads/atomic.wast",
374                "spec_testsuite/proposals/threads/exports.wast",
375                "spec_testsuite/proposals/threads/memory.wast",
376            ];
377
378            if unsupported.iter().any(|part| self.path.ends_with(part)) {
379                return true;
380            }
381        }
382
383        if config.compiler.should_fail(&self.config) {
384            return true;
385        }
386
387        // Disable spec tests for proposals that Winch does not implement yet.
388        if config.compiler == Compiler::Winch {
389            let unsupported = [
390                // externref/reference-types related
391                "component-model/modules.wast",
392                "extended-const/elem.wast",
393                "extended-const/global.wast",
394                "misc_testsuite/externref-id-function.wast",
395                "misc_testsuite/externref-segment.wast",
396                "misc_testsuite/externref-segments.wast",
397                "misc_testsuite/externref-table-dropped-segment-issue-8281.wast",
398                "misc_testsuite/linking-errors.wast",
399                "misc_testsuite/many_table_gets_lead_to_gc.wast",
400                "misc_testsuite/mutable_externref_globals.wast",
401                "misc_testsuite/no-mixup-stack-maps.wast",
402                "misc_testsuite/no-panic.wast",
403                "misc_testsuite/simple_ref_is_null.wast",
404                "misc_testsuite/table_grow_with_funcref.wast",
405                "spec_testsuite/br_table.wast",
406                "spec_testsuite/data-invalid.wast",
407                "spec_testsuite/elem.wast",
408                "spec_testsuite/global.wast",
409                "spec_testsuite/linking.wast",
410                "spec_testsuite/ref_func.wast",
411                "spec_testsuite/ref_is_null.wast",
412                "spec_testsuite/ref_null.wast",
413                "spec_testsuite/select.wast",
414                "spec_testsuite/table_fill.wast",
415                "spec_testsuite/table_get.wast",
416                "spec_testsuite/table_grow.wast",
417                "spec_testsuite/table_set.wast",
418                "spec_testsuite/table_size.wast",
419                // simd-related failures
420                "misc_testsuite/simd/canonicalize-nan.wast",
421            ];
422
423            if unsupported.iter().any(|part| self.path.ends_with(part)) {
424                return true;
425            }
426
427            // SIMD on Winch requires AVX instructions.
428            #[cfg(target_arch = "x86_64")]
429            if !(std::is_x86_feature_detected!("avx") && std::is_x86_feature_detected!("avx2")) {
430                let unsupported = [
431                    "annotations/simd_lane.wast",
432                    "memory64/simd.wast",
433                    "misc_testsuite/int-to-float-splat.wast",
434                    "misc_testsuite/issue6562.wast",
435                    "misc_testsuite/simd/almost-extmul.wast",
436                    "misc_testsuite/simd/cvt-from-uint.wast",
437                    "misc_testsuite/simd/issue_3327_bnot_lowering.wast",
438                    "misc_testsuite/simd/issue6725-no-egraph-panic.wast",
439                    "misc_testsuite/simd/replace-lane-preserve.wast",
440                    "misc_testsuite/simd/spillslot-size-fuzzbug.wast",
441                    "misc_testsuite/winch/issue-10331.wast",
442                    "misc_testsuite/winch/replace_lane.wast",
443                    "spec_testsuite/simd_align.wast",
444                    "spec_testsuite/simd_boolean.wast",
445                    "spec_testsuite/simd_conversions.wast",
446                    "spec_testsuite/simd_f32x4.wast",
447                    "spec_testsuite/simd_f32x4_arith.wast",
448                    "spec_testsuite/simd_f32x4_cmp.wast",
449                    "spec_testsuite/simd_f32x4_pmin_pmax.wast",
450                    "spec_testsuite/simd_f32x4_rounding.wast",
451                    "spec_testsuite/simd_f64x2.wast",
452                    "spec_testsuite/simd_f64x2_arith.wast",
453                    "spec_testsuite/simd_f64x2_cmp.wast",
454                    "spec_testsuite/simd_f64x2_pmin_pmax.wast",
455                    "spec_testsuite/simd_f64x2_rounding.wast",
456                    "spec_testsuite/simd_i16x8_cmp.wast",
457                    "spec_testsuite/simd_i32x4_cmp.wast",
458                    "spec_testsuite/simd_i64x2_arith2.wast",
459                    "spec_testsuite/simd_i64x2_cmp.wast",
460                    "spec_testsuite/simd_i8x16_arith2.wast",
461                    "spec_testsuite/simd_i8x16_cmp.wast",
462                    "spec_testsuite/simd_int_to_int_extend.wast",
463                    "spec_testsuite/simd_load.wast",
464                    "spec_testsuite/simd_load_extend.wast",
465                    "spec_testsuite/simd_load_splat.wast",
466                    "spec_testsuite/simd_load_zero.wast",
467                    "spec_testsuite/simd_splat.wast",
468                    "spec_testsuite/simd_store16_lane.wast",
469                    "spec_testsuite/simd_store32_lane.wast",
470                    "spec_testsuite/simd_store64_lane.wast",
471                    "spec_testsuite/simd_store8_lane.wast",
472                    "spec_testsuite/simd_load16_lane.wast",
473                    "spec_testsuite/simd_load32_lane.wast",
474                    "spec_testsuite/simd_load64_lane.wast",
475                    "spec_testsuite/simd_load8_lane.wast",
476                    "spec_testsuite/simd_bitwise.wast",
477                    "misc_testsuite/simd/load_splat_out_of_bounds.wast",
478                    "misc_testsuite/simd/unaligned-load.wast",
479                    "multi-memory/simd_memory-multi.wast",
480                    "misc_testsuite/simd/issue4807.wast",
481                    "spec_testsuite/simd_const.wast",
482                    "spec_testsuite/simd_i8x16_sat_arith.wast",
483                    "spec_testsuite/simd_i64x2_arith.wast",
484                    "spec_testsuite/simd_i16x8_arith.wast",
485                    "spec_testsuite/simd_i16x8_arith2.wast",
486                    "spec_testsuite/simd_i16x8_q15mulr_sat_s.wast",
487                    "spec_testsuite/simd_i16x8_sat_arith.wast",
488                    "spec_testsuite/simd_i32x4_arith.wast",
489                    "spec_testsuite/simd_i32x4_dot_i16x8.wast",
490                    "spec_testsuite/simd_i32x4_trunc_sat_f32x4.wast",
491                    "spec_testsuite/simd_i32x4_trunc_sat_f64x2.wast",
492                    "spec_testsuite/simd_i8x16_arith.wast",
493                    "spec_testsuite/simd_bit_shift.wast",
494                    "spec_testsuite/simd_lane.wast",
495                    "spec_testsuite/simd_i16x8_extmul_i8x16.wast",
496                    "spec_testsuite/simd_i32x4_extmul_i16x8.wast",
497                    "spec_testsuite/simd_i64x2_extmul_i32x4.wast",
498                    "spec_testsuite/simd_i16x8_extadd_pairwise_i8x16.wast",
499                    "spec_testsuite/simd_i32x4_extadd_pairwise_i16x8.wast",
500                    "spec_testsuite/simd_i32x4_arith2.wast",
501                ];
502
503                if unsupported.iter().any(|part| self.path.ends_with(part)) {
504                    return true;
505                }
506            }
507        }
508
509        for part in self.path.iter() {
510            // Not implemented in Wasmtime yet
511            if part == "exception-handling" {
512                return !self.path.ends_with("binary.wast") && !self.path.ends_with("exports.wast");
513            }
514
515            if part == "memory64" {
516                if [
517                    // wasmtime doesn't implement exceptions yet
518                    "imports.wast",
519                    "ref_null.wast",
520                    "throw.wast",
521                    "throw_ref.wast",
522                    "try_table.wast",
523                    "tag.wast",
524                    "instance.wast",
525                ]
526                .iter()
527                .any(|i| self.path.ends_with(i))
528                {
529                    return true;
530                }
531            }
532        }
533
534        false
535    }
536}
537
538fn spec_proposal_from_path(path: &Path) -> Option<&str> {
539    let mut iter = path.iter();
540    loop {
541        match iter.next()?.to_str()? {
542            "proposals" => break,
543            _ => {}
544        }
545    }
546    Some(iter.next()?.to_str()?)
547}