├── .envrc ├── ci-tools ├── .gitignore ├── Dockerfile └── start_nix.sh ├── pkgs ├── whitepaper │ ├── .gitignore │ ├── package.nix │ ├── index.html │ ├── template.typ │ └── refs.yaml ├── requirements.nix ├── benchmark.nix ├── typst-packages-cache.nix ├── coverage.nix ├── report │ ├── package.nix │ └── report_index.html └── wasm-interpreter.nix ├── crates ├── compare-testsuite-rs │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ ├── ci_reports.rs │ │ ├── main.rs │ │ └── summary.rs ├── log_wrapper │ ├── README.md │ ├── Cargo.toml │ └── src │ │ └── lib.rs └── benchmark │ ├── Cargo.toml │ └── README.md ├── tests ├── lib.rs ├── structured_control_flow │ ├── mod.rs │ └── loop.rs ├── arithmetic │ ├── mod.rs │ ├── subtraction.rs │ └── multiply.rs ├── errors.rs ├── wasm_spec_testsuite.rs ├── module_instantiate.rs ├── start_function.rs ├── return.rs ├── same_type_fn.rs ├── dynamic.rs ├── specification │ ├── files.rs │ ├── README.md │ ├── ci_reports.rs │ ├── mod.rs │ └── reports.rs ├── user_data.rs ├── add_one.rs ├── rw_spinlock.rs ├── memory_fill.rs ├── basic_memory.rs ├── select.rs ├── function_recursion.rs ├── memory_redundancy.rs ├── memory_trap.rs ├── linker.rs ├── table_get.rs ├── globals.rs └── table_size.rs ├── .gitignore ├── .gitmodules ├── src ├── core │ ├── mod.rs │ ├── reader │ │ ├── types │ │ │ ├── memarg.rs │ │ │ ├── global.rs │ │ │ ├── import.rs │ │ │ ├── data.rs │ │ │ └── export.rs │ │ └── section_header.rs │ ├── utils.rs │ ├── indices.rs │ ├── sidetable.rs │ ├── slotmap.rs │ └── rw_spinlock.rs ├── execution │ ├── assert_validated.rs │ ├── mod.rs │ ├── little_endian.rs │ ├── resumable.rs │ ├── config.rs │ ├── const_interpreter_loop.rs │ └── checked │ │ └── value.rs ├── validation │ ├── globals.rs │ └── data.rs └── lib.rs ├── .github ├── workflows │ ├── labeler.yaml │ ├── pages_requirement_preview.yaml │ ├── pages_whitepaper_preview.yaml │ ├── no-nix-ci.yaml │ ├── pages_deploy_main.yaml │ ├── testsuite_preview.yaml │ ├── pages_coverage_preview.yaml │ └── nix.yaml ├── pull_request_template.md └── labeler.yml ├── .config └── nextest.toml ├── treefmt.nix ├── overlay.nix ├── LICENSE-MIT ├── Cargo.toml ├── examples └── stuff │ └── main.rs ├── README.md └── flake.lock /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /ci-tools/.gitignore: -------------------------------------------------------------------------------- 1 | nix-sharedfs 2 | -------------------------------------------------------------------------------- /pkgs/whitepaper/.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | -------------------------------------------------------------------------------- /crates/compare-testsuite-rs/.gitignore: -------------------------------------------------------------------------------- 1 | target/ -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | mod arithmetic; 2 | mod structured_control_flow; 3 | -------------------------------------------------------------------------------- /tests/structured_control_flow/mod.rs: -------------------------------------------------------------------------------- 1 | mod block; 2 | mod r#if; 3 | mod r#loop; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .direnv/ 2 | /output 3 | /requirements/export 4 | /target 5 | result 6 | testsuite_results.json 7 | -------------------------------------------------------------------------------- /tests/arithmetic/mod.rs: -------------------------------------------------------------------------------- 1 | mod bitwise; 2 | mod division; 3 | mod multiply; 4 | mod remainder; 5 | mod subtraction; 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/specification/testsuite"] 2 | path = tests/specification/testsuite 3 | url = https://github.com/WebAssembly/testsuite.git 4 | -------------------------------------------------------------------------------- /ci-tools/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nixos/nix:latest 2 | 3 | RUN mkdir -p /etc/nix && \ 4 | echo "experimental-features = nix-command flakes" >> /etc/nix/nix.conf 5 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod error; 2 | 3 | pub mod indices; 4 | pub mod reader; 5 | pub mod rw_spinlock; 6 | pub mod sidetable; 7 | pub mod slotmap; 8 | pub mod utils; 9 | -------------------------------------------------------------------------------- /pkgs/whitepaper/package.nix: -------------------------------------------------------------------------------- 1 | { buildTypstProject, typst-packages-cache }: 2 | 3 | buildTypstProject { 4 | name = "whitepaper.pdf"; 5 | src = ./.; 6 | XDG_CACHE_HOME = typst-packages-cache; 7 | } 8 | -------------------------------------------------------------------------------- /tests/errors.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use wasm::{RuntimeError, TrapError}; 3 | 4 | #[test_log::test] 5 | pub fn runtime_error_bad_conversion_to_integer() { 6 | info!("{}", RuntimeError::Trap(TrapError::BadConversionToInteger)) 7 | } 8 | -------------------------------------------------------------------------------- /crates/log_wrapper/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The purpose of this crate is to provide a configurable logging backend, whose logging macros either resolve to nothing (empty statements), or to a downstream [`log`](https://docs.rs/log/) implementation. 4 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yaml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | 5 | jobs: 6 | labeler: 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/labeler@v5 13 | -------------------------------------------------------------------------------- /tests/wasm_spec_testsuite.rs: -------------------------------------------------------------------------------- 1 | // The reason this file exists is only to expose the `specification` module to the outside world. 2 | // More so, the reason it wasn't added to the `lib.rs` file is because we wanted to separate the 3 | // regular tests from the spec tests. 4 | 5 | mod specification; 6 | -------------------------------------------------------------------------------- /pkgs/requirements.nix: -------------------------------------------------------------------------------- 1 | { 2 | runCommand, 3 | strictdoc, 4 | flakeRoot, 5 | }: 6 | 7 | runCommand "compile-requirements" { nativeBuildInputs = [ strictdoc ]; } '' 8 | strictdoc export --formats html,json --enable-mathjax ${flakeRoot + "/requirements"} 9 | mv -- output "$out" 10 | '' 11 | -------------------------------------------------------------------------------- /crates/compare-testsuite-rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "compare-testsuite-rs" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.74.1" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | clap = { version = "4", features = ["derive"] } 11 | serde = { version = "1", features = ["derive"] } 12 | serde_json = "1" 13 | anyhow = "1" 14 | itertools = "0.14" 15 | -------------------------------------------------------------------------------- /.config/nextest.toml: -------------------------------------------------------------------------------- 1 | [profile.ci] 2 | # Print out output for failing tests as soon as they fail, and also at the end 3 | # of the run (for easy scrollability). 4 | failure-output = "immediate-final" 5 | # Do not cancel the test run on the first failure. 6 | fail-fast = false 7 | 8 | [profile.ci.junit] 9 | # Output a JUnit report into the given file inside 'store.dir/'. 10 | # If unspecified, JUnit is not written out. 11 | path = "junit.xml" 12 | -------------------------------------------------------------------------------- /crates/log_wrapper/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "log_wrapper" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.76.0" # Keep this in sync with the requirements! 6 | description = """ 7 | A WASM interpreter tailored for safety use-cases, such as automotive and avionics applications 8 | """ 9 | homepage = "https://github.com/DLR-FT/wasm-interpreter" 10 | license = "MIT OR Apache-2.0" 11 | 12 | [dependencies] 13 | log = { workspace = true, optional = true } 14 | -------------------------------------------------------------------------------- /pkgs/whitepaper/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Wasm Interpreter Whitepaper 7 | 10 | 11 | 12 | 13 | If you are seeing this page, follow this link 14 | to the whitepaper. 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/core/reader/types/memarg.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::Debug; 2 | 3 | use crate::{ 4 | core::reader::{WasmReadable, WasmReader}, 5 | ValidationError, 6 | }; 7 | 8 | #[derive(Debug)] 9 | pub struct MemArg { 10 | pub offset: u32, 11 | pub align: u32, 12 | } 13 | 14 | impl WasmReadable for MemArg { 15 | fn read(wasm: &mut WasmReader) -> Result { 16 | let align = wasm.read_var_u32()?; 17 | let offset = wasm.read_var_u32()?; 18 | Ok(Self { offset, align }) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pkgs/benchmark.nix: -------------------------------------------------------------------------------- 1 | { wasm-interpreter }: 2 | 3 | wasm-interpreter.overrideAttrs (old: { 4 | pname = old.pname + "-benchmark"; 5 | cargoBuildFlags = [ "--package=benchmark" ]; 6 | 7 | postBuild = '' 8 | pushd crates/benchmark 9 | cargo bench 10 | popd 11 | ''; 12 | 13 | doCheck = false; 14 | 15 | # TODO add preInstall and postInstlal hook 16 | installPhase = '' 17 | runHook preInstall 18 | 19 | shopt -s globstar 20 | mv target/**/criterion/ "$out/" 21 | shopt -u globstar 22 | 23 | runHook postInstall 24 | ''; 25 | }) 26 | -------------------------------------------------------------------------------- /pkgs/typst-packages-cache.nix: -------------------------------------------------------------------------------- 1 | { stdenvNoCC, fetchFromGitHub }: 2 | 3 | stdenvNoCC.mkDerivation { 4 | name = "typst-packages-cache"; 5 | src = fetchFromGitHub { 6 | owner = "typst"; 7 | repo = "packages"; 8 | rev = "aa0d7b808aa3999f6e854f7800ada584b8eee0fa"; # updated on 2025-05-27 9 | hash = "sha256-pqqiDSXu/j6YV3RdYHPzW15jBKAhdUtseCH6tLMRizg="; 10 | }; 11 | dontBuild = true; 12 | installPhase = '' 13 | mkdir --parent -- "$out/typst/packages" 14 | cp --dereference --no-preserve=mode --recursive --reflink=auto \ 15 | --target-directory="$out/typst/packages" -- "$src"/packages/* 16 | ''; 17 | } 18 | -------------------------------------------------------------------------------- /treefmt.nix: -------------------------------------------------------------------------------- 1 | { lib, ... }: 2 | { 3 | # Used to find the project root 4 | projectRootFile = "flake.nix"; 5 | settings.global.excludes = [ 6 | "tests/specification/testsuite/*" 7 | ]; 8 | programs.nixfmt.enable = true; 9 | programs.prettier = { 10 | enable = true; 11 | includes = [ 12 | "*.css" 13 | "*.html" 14 | "*.js" 15 | "*.json" 16 | "*.json5" 17 | "*.md" 18 | "*.mdx" 19 | "*.yaml" 20 | "*.yml" 21 | ]; 22 | }; 23 | programs.rustfmt = { 24 | enable = true; 25 | edition = (lib.importTOML ./Cargo.toml).package.edition; 26 | }; 27 | programs.taplo.enable = true; # formats TOML files 28 | programs.typstfmt.enable = true; 29 | } 30 | -------------------------------------------------------------------------------- /tests/module_instantiate.rs: -------------------------------------------------------------------------------- 1 | use wasm::{validate, RuntimeError, Store}; 2 | 3 | #[test_log::test] 4 | fn use_incorrect_number_of_extern_vals() { 5 | const MODULE_WITH_IMPORTS: &str = r#" 6 | (module 7 | (import "host" "foo" (func $foo)) 8 | (import "host" "bar" (global $bar (mut i32))) 9 | ) 10 | "#; 11 | 12 | let wasm_bytes = wat::parse_str(MODULE_WITH_IMPORTS).unwrap(); 13 | let validation_info = validate(&wasm_bytes).unwrap(); 14 | 15 | let mut store = Store::new(()); 16 | 17 | assert_eq!( 18 | store 19 | .module_instantiate(&validation_info, Vec::new(), None) 20 | .err(), 21 | Some(RuntimeError::ExternValsLenMismatch) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /overlay.nix: -------------------------------------------------------------------------------- 1 | final: prev: 2 | let 3 | inherit (prev) lib; 4 | 5 | # root dir of this flake 6 | flakeRoot = ./.; 7 | 8 | # all packages from the local tree 9 | wasm-interpreter-pkgs = lib.filesystem.packagesFromDirectoryRecursive { 10 | # a special callPackage variant that contains our flakeRoot 11 | callPackage = lib.callPackageWith (final // { inherit flakeRoot; }); 12 | 13 | # local tree of packages 14 | directory = ./pkgs; 15 | }; 16 | in 17 | 18 | { 19 | # https://github.com/NixOS/nixpkgs/pull/42637 20 | requireFile = 21 | args: 22 | (prev.requireFile args).overrideAttrs (_: { 23 | allowSubstitutes = true; 24 | }); 25 | 26 | # custom namespace for packages from the local tree 27 | inherit wasm-interpreter-pkgs; 28 | } 29 | // wasm-interpreter-pkgs 30 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Pull Request Overview 2 | 3 | 6 | 7 | ### TODO or Help Wanted 8 | 9 | 12 | 13 | ### Checks 14 | 15 | 18 | 19 | - Using Nix 20 | - [ ] Ran `nix fmt` 21 | - [ ] Ran `nix flake check '.?submodules=1'` 22 | - Using Rust tooling 23 | - [ ] Ran `cargo fmt` 24 | - [ ] Ran `cargo test` 25 | - [ ] Ran `cargo check` 26 | - [ ] Ran `cargo build` 27 | - [ ] Ran `cargo doc` 28 | 29 | ### Benchmark Results 30 | 31 | 36 | 37 | ### Github Issue 38 | 39 | 42 | -------------------------------------------------------------------------------- /crates/benchmark/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "benchmark" 3 | version = "0.1.0" 4 | edition = "2021" 5 | rust-version = "1.80.0" 6 | 7 | [dev-dependencies] 8 | criterion = { version = "0.5.1", default-features = false, features = [ 9 | "cargo_bench_support", 10 | "plotters", 11 | ] } 12 | wasm-interpreter = { path = "../..", default-features = false } 13 | wasmi = "0.40.0" 14 | wasmtime = { version = "27.0.0", default-features = false, features = [ 15 | "runtime", 16 | "cranelift", 17 | "parallel-compilation", 18 | "std", 19 | ] } 20 | wat = { workspace = true } 21 | 22 | [features] 23 | foreign-interpreters = ["wasmi", "wasmtime"] 24 | wasmi = [] 25 | wasmtime = [] 26 | 27 | [[bench]] 28 | name = "general_purpose" 29 | harness = false 30 | 31 | [[bench]] 32 | name = "var_length_integer_reading" 33 | harness = false 34 | -------------------------------------------------------------------------------- /crates/log_wrapper/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | #[cfg(feature = "log")] 4 | pub use ::log::{debug, error, info, trace, warn}; 5 | 6 | #[cfg(not(feature = "log"))] 7 | mod log_noop { 8 | /// Noop, expands to nothing 9 | #[macro_export] 10 | macro_rules! error { 11 | ($($arg:tt)+) => {{}}; 12 | } 13 | 14 | /// Noop, expands to nothing 15 | #[macro_export] 16 | macro_rules! warn { 17 | ($($arg:tt)+) => {{}}; 18 | } 19 | 20 | /// Noop, expands to nothing 21 | #[macro_export] 22 | macro_rules! info { 23 | ($($arg:tt)+) => {{}}; 24 | } 25 | 26 | /// Noop, expands to nothing 27 | #[macro_export] 28 | macro_rules! debug { 29 | ($($arg:tt)+) => {{}}; 30 | } 31 | 32 | /// Noop, expands to nothing 33 | #[macro_export] 34 | macro_rules! trace { 35 | ($($arg:tt)+) => {{}}; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /crates/compare-testsuite-rs/src/ci_reports.rs: -------------------------------------------------------------------------------- 1 | /// Interfaces copied from the main project and annotated with `serde::Deserialize` and `Debug`. 2 | use serde::Deserialize; 3 | 4 | #[derive(Deserialize, Debug)] 5 | pub struct CIFullReport { 6 | pub entries: Vec, 7 | } 8 | 9 | #[derive(Deserialize, Debug)] 10 | pub struct CIReportHeader { 11 | pub filepath: String, 12 | pub data: CIReportData, 13 | } 14 | 15 | #[derive(Deserialize, Debug)] 16 | pub enum CIReportData { 17 | Assert { 18 | results: Vec, 19 | }, 20 | ScriptError { 21 | error: String, 22 | context: String, 23 | line_number: Option, 24 | _command: Option, 25 | }, 26 | } 27 | 28 | #[derive(Deserialize, Debug)] 29 | pub struct CIAssert { 30 | pub error: Option, 31 | pub line_number: u32, 32 | pub command: String, 33 | } 34 | -------------------------------------------------------------------------------- /src/core/reader/types/global.rs: -------------------------------------------------------------------------------- 1 | use crate::core::reader::span::Span; 2 | use crate::core::reader::types::ValType; 3 | use crate::core::reader::{WasmReadable, WasmReader}; 4 | use crate::ValidationError; 5 | 6 | #[derive(Debug, Copy, Clone)] 7 | pub struct Global { 8 | pub ty: GlobalType, 9 | // TODO validate init_expr during validation and execute during instantiation 10 | pub init_expr: Span, 11 | } 12 | 13 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 14 | pub struct GlobalType { 15 | pub ty: ValType, 16 | pub is_mut: bool, 17 | } 18 | 19 | impl WasmReadable for GlobalType { 20 | fn read(wasm: &mut WasmReader) -> Result { 21 | let ty = ValType::read(wasm)?; 22 | let is_mut = match wasm.read_u8()? { 23 | 0x00 => false, 24 | 0x01 => true, 25 | other => return Err(ValidationError::MalformedMutDiscriminator(other)), 26 | }; 27 | Ok(Self { ty, is_mut }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/execution/assert_validated.rs: -------------------------------------------------------------------------------- 1 | //! Helpers for assertions due to prior validation of a WASM program. 2 | 3 | use core::fmt::Debug; 4 | 5 | pub(crate) trait UnwrapValidatedExt { 6 | fn unwrap_validated(self) -> T; 7 | } 8 | 9 | impl UnwrapValidatedExt for Option { 10 | /// Indicate that we can assume this Option to be Some(_) due to prior validation 11 | fn unwrap_validated(self) -> T { 12 | self.expect("Validation guarantees this to be `Some(_)`, but it is `None`") 13 | } 14 | } 15 | 16 | impl UnwrapValidatedExt for Result { 17 | /// Indicate that we can assume this Result to be Ok(_) due to prior validation 18 | fn unwrap_validated(self) -> T { 19 | self.unwrap_or_else(|e| { 20 | panic!("Validation guarantees this to be `Ok(_)`, but it is `Err({e:?})`"); 21 | }) 22 | } 23 | } 24 | 25 | #[macro_export] 26 | macro_rules! unreachable_validated { 27 | () => { 28 | unreachable!("because of prior validation") 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /pkgs/coverage.nix: -------------------------------------------------------------------------------- 1 | { wasm-interpreter, cargo-llvm-cov }: 2 | 3 | wasm-interpreter.overrideAttrs (old: { 4 | pname = old.pname + "-coverage"; 5 | 6 | nativeCheckInputs = [ cargo-llvm-cov ]; 7 | 8 | env = { 9 | inherit (cargo-llvm-cov) LLVM_COV LLVM_PROFDATA; 10 | }; 11 | 12 | dontBuild = true; 13 | 14 | checkPhase = '' 15 | runHook preCheck 16 | 17 | cargo llvm-cov --no-report nextest --no-default-features 18 | 19 | runHook postCheck 20 | ''; 21 | 22 | installPhase = '' 23 | runHook preInstall 24 | 25 | cargo llvm-cov report --lcov --output-path lcov.info 26 | cargo llvm-cov report --json --output-path lcov.json 27 | cargo llvm-cov report --cobertura --output-path lcov-cobertura.xml 28 | cargo llvm-cov report --codecov --output-path lcov-codecov.json 29 | cargo llvm-cov report --text --output-path lcov.txt 30 | cargo llvm-cov report --html --output-dir lcov-html 31 | 32 | mkdir --parent -- "$out" 33 | mv lcov* "$out/" 34 | 35 | runHook postInstall 36 | ''; 37 | }) 38 | -------------------------------------------------------------------------------- /crates/benchmark/README.md: -------------------------------------------------------------------------------- 1 | # Note 2 | 3 | Do not alter existing benchmarks in any measurable way! Do not rename them! New benchmark, new name! We want to be able to track the performance over a long time, and this is only possible if existing benchmarks are not tinkered with. 4 | 5 | # Benchmarks 6 | 7 | - **general_purpose**: General purpose benchmark for interpreter speed. Consists of the following bench-functions: 8 | - **fibonacci_recursive**: benches function creation, as due to its recursive nature new call-frames are created and removed very often. 9 | - **fibonacci_loop**: benches the control flow between blocks within a single function call-frame. 10 | 11 | # How to bench 12 | 13 | ``` 14 | # Benchmark just our interpreter 15 | cargo bench --bench general_purpose 16 | 17 | 18 | # Benchmark our interpreter vs. wasmi vs. wasmtime 19 | cargo bench --features wasmi,wasmtime --bench general_purpose 20 | 21 | 22 | # Analyze the impact of changes on your current branch (including unstaged modifications!) vs main 23 | bench-against-main 24 | ``` 25 | -------------------------------------------------------------------------------- /pkgs/whitepaper/template.typ: -------------------------------------------------------------------------------- 1 | #import "@preview/ccicons:1.0.1": cc-by-sa 2 | 3 | #let setup_template(title: [], author: [], keywords: (), affiliation: [], contents) = { 4 | set document(title: title, author: author, keywords: keywords) 5 | set page( 6 | paper: "a4", columns: 1, header: context{ 7 | if counter(page).get().first() > 1 { 8 | align(right, title) 9 | } 10 | }, footer: context{ 11 | set text(8pt) 12 | let left_footer = [ 13 | License: #link("https://creativecommons.org/licenses/by-sa/4.0/")[CC-BY-SA #cc-by-sa] \ 14 | Copyright © 2024-#datetime.today().year() German Aerospace Center (DLR). All 15 | rights reserved. 16 | ] 17 | let right_footer = counter(page).display("1 of 1", both: true) 18 | 19 | grid(columns: (1fr, auto), align: (left, right), left_footer, right_footer) 20 | }, 21 | ) 22 | 23 | // Style 24 | set heading(numbering: "1.") 25 | 26 | align(center, text(17pt)[*#title*]) 27 | 28 | grid(columns: (1fr), align(center)[ 29 | #author \ 30 | #affiliation 31 | ]) 32 | 33 | contents 34 | } 35 | -------------------------------------------------------------------------------- /src/core/utils.rs: -------------------------------------------------------------------------------- 1 | // TODO Reconsider the importance of this module. All of its functions do the same? 2 | 3 | #[cfg(debug_assertions)] 4 | pub fn print_beautiful_instruction_name_1_byte(first_byte: u8, pc: usize) { 5 | use crate::core::reader::types::opcode::opcode_byte_to_str; 6 | 7 | trace!( 8 | "Read instruction {} at wasm_binary[{}]", 9 | opcode_byte_to_str(first_byte), 10 | pc 11 | ); 12 | } 13 | 14 | #[cfg(debug_assertions)] 15 | pub fn print_beautiful_fc_extension(second_byte: u32, pc: usize) { 16 | use crate::core::reader::types::opcode::fc_extension_opcode_to_str; 17 | 18 | trace!( 19 | "Read instruction {} at wasm_binary[{}]", 20 | fc_extension_opcode_to_str(second_byte), 21 | pc, 22 | ); 23 | } 24 | 25 | #[cfg(debug_assertions)] 26 | pub fn print_beautiful_fd_extension(second_byte: u32, pc: usize) { 27 | use crate::core::reader::types::opcode::fd_extension_opcode_to_str; 28 | 29 | trace!( 30 | "Read instruction {} at wasm_binary[{}]", 31 | fd_extension_opcode_to_str(second_byte), 32 | pc, 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/core/indices.rs: -------------------------------------------------------------------------------- 1 | // /// This macro defines index types. Currently (2024-06-10) all indices are [`u32`]. 2 | // /// See for more information. 3 | // macro_rules! def_idx_types { 4 | // ($($name:ident),*) => { 5 | // $( 6 | // /// 7 | // pub type $name = usize; 8 | // )* 9 | // }; 10 | // } 11 | 12 | // // #[allow(dead_code)] 13 | // def_idx_types!(TypeIdx, FuncIdx, TableIdx, MemIdx, GlobalIdx, /* ElemIdx, DataIdx, */ LocalIdx/* , LabelIdx */); 14 | 15 | // TODO check whether is is clever to internally use usize instead of u32; potential problems are: 16 | // - unsound on architectures where `usize` < `u32` 17 | // - wasteful in memory on architectures where `usize` > `u32` 18 | pub type TypeIdx = usize; 19 | pub type FuncIdx = usize; 20 | pub type TableIdx = usize; 21 | pub type MemIdx = usize; 22 | pub type GlobalIdx = usize; 23 | #[allow(dead_code)] 24 | pub type ElemIdx = usize; 25 | pub type DataIdx = usize; 26 | pub type LocalIdx = usize; 27 | #[allow(dead_code)] 28 | pub type LabelIdx = usize; 29 | -------------------------------------------------------------------------------- /tests/start_function.rs: -------------------------------------------------------------------------------- 1 | //! The WASM program stores 42 into linear memory upon instantiation through a start function. 2 | //! Then it reads the same value and checks its value. 3 | 4 | use wasm::{validate, Store}; 5 | 6 | #[test_log::test] 7 | fn start_function() { 8 | let wat = r#" 9 | (module 10 | (memory 1) 11 | 12 | (func $store42 13 | i32.const 0 14 | i32.const 42 15 | i32.store) 16 | 17 | (start $store42) 18 | 19 | (func (export "load_num") (result i32) 20 | i32.const 0 21 | i32.load) 22 | ) 23 | "#; 24 | let wasm_bytes = wat::parse_str(wat).unwrap(); 25 | 26 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 27 | let mut store = Store::new(()); 28 | let module = store 29 | .module_instantiate(&validation_info, Vec::new(), None) 30 | .unwrap() 31 | .module_addr; 32 | 33 | let load_num = store 34 | .instance_export(module, "load_num") 35 | .unwrap() 36 | .as_func() 37 | .unwrap(); 38 | 39 | assert_eq!(42, store.invoke_typed_without_fuel(load_num, ()).unwrap()); 40 | } 41 | -------------------------------------------------------------------------------- /pkgs/report/package.nix: -------------------------------------------------------------------------------- 1 | { 2 | stdenvNoCC, 3 | python3Packages, 4 | wasm-interpreter-pkgs, 5 | }: 6 | 7 | stdenvNoCC.mkDerivation { 8 | pname = wasm-interpreter-pkgs.wasm-interpreter.pname + "-report"; 9 | version = wasm-interpreter-pkgs.wasm-interpreter.version; 10 | dontUnpack = true; 11 | 12 | nativeBuildInputs = [ 13 | python3Packages.junit2html 14 | ]; 15 | 16 | installPhase = '' 17 | runHook preInstall 18 | 19 | mkdir -- "$out" 20 | pushd "$out" 21 | 22 | cp --recursive -- ${wasm-interpreter-pkgs.benchmark} bench 23 | cp --recursive -- ${wasm-interpreter-pkgs.coverage}/lcov-html coverage 24 | cp --recursive -- ${wasm-interpreter-pkgs.requirements} requirements 25 | cp --recursive -- ${ 26 | wasm-interpreter-pkgs.wasm-interpreter.override { doDoc = true; } 27 | }/share/doc/ rustdoc 28 | cp --dereference -- ${wasm-interpreter-pkgs.whitepaper} whitepaper.pdf 29 | 30 | mkdir test 31 | junit2html ${ 32 | wasm-interpreter-pkgs.wasm-interpreter.override { useNextest = true; } 33 | }/junit.xml test/index.html 34 | 35 | 36 | cp ${./report_index.html} index.html 37 | 38 | popd 39 | 40 | runHook postInstall 41 | ''; 42 | } 43 | -------------------------------------------------------------------------------- /tests/return.rs: -------------------------------------------------------------------------------- 1 | use wasm::{validate, Store}; 2 | 3 | /// A simple function to add 2 two i32s but using the RETURN opcode. 4 | #[test_log::test] 5 | fn return_valid() { 6 | let wat = r#" 7 | (module 8 | (func (export "add") (param $x i32) (param $y i32) (result i32) 9 | local.get $x 10 | local.get $x 11 | local.get $x 12 | local.get $x 13 | local.get $x 14 | local.get $x 15 | local.get $x 16 | local.get $y 17 | i32.add 18 | return 19 | ) 20 | ) 21 | "#; 22 | let wasm_bytes = wat::parse_str(wat).unwrap(); 23 | 24 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 25 | let mut store = Store::new(()); 26 | let module = store 27 | .module_instantiate(&validation_info, Vec::new(), None) 28 | .unwrap() 29 | .module_addr; 30 | 31 | let add = store 32 | .instance_export(module, "add") 33 | .unwrap() 34 | .as_func() 35 | .unwrap(); 36 | 37 | assert_eq!(12, store.invoke_typed_without_fuel(add, (10, 2)).unwrap()); 38 | assert_eq!(2, store.invoke_typed_without_fuel(add, (0, 2)).unwrap()); 39 | assert_eq!(-4, store.invoke_typed_without_fuel(add, (-6, 2)).unwrap()); 40 | } 41 | -------------------------------------------------------------------------------- /tests/same_type_fn.rs: -------------------------------------------------------------------------------- 1 | use wasm::{validate, Store}; 2 | 3 | /// This test checks if we can validate and executa a module which has two functions with the same signature. 4 | #[test_log::test] 5 | fn same_type_fn() { 6 | let wat = r#" 7 | (module 8 | (func (export "add_one") (param $x i32) (result i32) 9 | local.get $x 10 | i32.const 1 11 | i32.add) 12 | 13 | (func (export "add_two") (param $x i32) (result i32) 14 | local.get $x 15 | i32.const 2 16 | i32.add) 17 | ) 18 | "#; 19 | let wasm_bytes = wat::parse_str(wat).unwrap(); 20 | 21 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 22 | let mut store = Store::new(()); 23 | let module = store 24 | .module_instantiate(&validation_info, Vec::new(), None) 25 | .unwrap() 26 | .module_addr; 27 | 28 | let add_one = store 29 | .instance_export(module, "add_one") 30 | .unwrap() 31 | .as_func() 32 | .unwrap(); 33 | let add_two = store 34 | .instance_export(module, "add_two") 35 | .unwrap() 36 | .as_func() 37 | .unwrap(); 38 | 39 | assert_eq!(-5, store.invoke_typed_without_fuel(add_one, -6).unwrap()); 40 | assert_eq!(-4, store.invoke_typed_without_fuel(add_two, -6).unwrap()); 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/pages_requirement_preview.yaml: -------------------------------------------------------------------------------- 1 | name: Requirement Preview Deploy 2 | 3 | on: 4 | # When a PR is merged (or force push to main) 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | - closed 11 | paths: 12 | - "requirements/**/" 13 | - ".github/workflows/pages_requirement_preview.yaml" 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | 20 | concurrency: preview-${{ github.ref }} 21 | 22 | jobs: 23 | deploy: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | submodules: true 29 | # -=-=-=-= Strictdoc =-=-=-=- 30 | - name: Install python 31 | uses: actions/setup-python@v5.1.0 32 | with: 33 | python-version: 3.12 34 | - name: Install strictdoc & requirements 35 | run: pip install strictdoc setuptools # Note: we need setuptools for strictdoc to work. Installing just strictdoc is not enough 36 | - name: Export requirements 37 | run: strictdoc export ./requirements/requirements.sdoc 38 | 39 | # -=-=-=-= Deploy =-=-=-=- 40 | - name: Deploy Preview 41 | uses: rossjrw/pr-preview-action@v1.4.7 42 | with: 43 | source-dir: output/html/ 44 | umbrella-dir: requirements/pr-preview 45 | -------------------------------------------------------------------------------- /.github/workflows/pages_whitepaper_preview.yaml: -------------------------------------------------------------------------------- 1 | name: Whitepaper Preview Deploy 2 | 3 | on: 4 | # When a PR is merged (or force push to main) 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | - closed 11 | paths: 12 | - "whitepaper/**/" 13 | - ".github/workflows/pages_whitepaper_preview.yaml" 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | 20 | concurrency: preview-${{ github.ref }} 21 | 22 | jobs: 23 | deploy: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | submodules: true 29 | # -=-=-=-= Strictdoc =-=-=-=- 30 | - uses: cachix/install-nix-action@v31 31 | with: 32 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 33 | - uses: cachix/cachix-action@v16 34 | with: 35 | name: dlr-ft 36 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 37 | - run: nix build .?submodules=1#whitepaper --print-build-logs 38 | 39 | - run: mkdir output && cp ./result ./output/whitepaper.pdf && cp ./pkgs/whitepaper/index.html ./output/index.html 40 | 41 | # -=-=-=-= Deploy =-=-=-=- 42 | - name: Deploy Preview 43 | uses: rossjrw/pr-preview-action@v1.4.7 44 | with: 45 | source-dir: output/ 46 | umbrella-dir: whitepaper/pr-preview 47 | -------------------------------------------------------------------------------- /tests/dynamic.rs: -------------------------------------------------------------------------------- 1 | use wasm::{checked::StoredValue, validate, Store}; 2 | 3 | /// A simple function to add two numbers and return the result, using [invoke_dynamic](wasm::RuntimeInstance::invoke_dynamic) 4 | /// instead of [invoke_named](wasm::RuntimeInstance::invoke_named). 5 | #[test_log::test] 6 | fn dynamic_add() { 7 | let wat = r#" 8 | (module 9 | (func (export "add") (param $x i32) (param $y i32) (result i32) 10 | local.get $x 11 | local.get $y 12 | i32.add) 13 | ) 14 | "#; 15 | let wasm_bytes = wat::parse_str(wat).unwrap(); 16 | 17 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 18 | let mut store = Store::new(()); 19 | let module = store 20 | .module_instantiate(&validation_info, Vec::new(), None) 21 | .unwrap() 22 | .module_addr; 23 | 24 | let add = store 25 | .instance_export(module, "add") 26 | .unwrap() 27 | .as_func() 28 | .unwrap(); 29 | 30 | let res = store 31 | .invoke_without_fuel(add, vec![StoredValue::I32(11), StoredValue::I32(1)]) 32 | .expect("invocation failed"); 33 | assert_eq!(vec![StoredValue::I32(12)], res); 34 | 35 | let res = store 36 | .invoke_without_fuel( 37 | add, 38 | vec![StoredValue::I32(-6i32 as u32), StoredValue::I32(1)], 39 | ) 40 | .expect("invocation failed"); 41 | assert_eq!(vec![StoredValue::I32(-5i32 as u32)], res); 42 | } 43 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | core: 2 | - changed-files: 3 | - any-glob-to-any-file: "src/core/**" 4 | execution: 5 | - changed-files: 6 | - any-glob-to-any-file: "src/execution/**" 7 | validation: 8 | - changed-files: 9 | - any-glob-to-any-file: "src/validation/**" 10 | 11 | tests: 12 | - changed-files: 13 | - any-glob-to-any-file: "tests/**" 14 | 15 | examples: 16 | - changed-files: 17 | - any-glob-to-any-file: "examples/**" 18 | 19 | priority-high: 20 | # Hotfixes are critical 21 | - head-branch: ["^hotfix", "hotfix"] 22 | - changed-files: 23 | - any-glob-to-any-file: [ 24 | "src/**", 25 | # We treat configuration modification as high priority 26 | # as I reckon we'll have critical CI/CD workflow changes 27 | # and other changes that are highly needed for a 28 | # healthy functioning of the project 29 | ".github/**", 30 | ] 31 | 32 | # We treat tests as medium priority (for now) 33 | # as we get to have specialized tests 34 | # (for functionality that is not critical) 35 | # we will then have different priorities for different tests 36 | priority-medium: 37 | - changed-files: 38 | - any-glob-to-any-file: "tests/**" 39 | 40 | priority-low: 41 | - changed-files: 42 | - any-glob-to-any-file: "examples/**" 43 | 44 | hotfix: 45 | - head-branch: ["^hotfix", "hotfix"] 46 | bugfix: 47 | - head-branch: ["^bugfix", "bugfix"] 48 | feature: 49 | - head-branch: ["^feature", "feature"] 50 | -------------------------------------------------------------------------------- /pkgs/report/report_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 42 | wasm-interpreter Report 43 | 44 | 45 | Documentation 46 | Requirements 47 | Tests 48 | Coverage 49 | Benchmark 50 | Whitepaper 51 | 52 | 53 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "crates/benchmark", 5 | "crates/compare-testsuite-rs", 6 | "crates/log_wrapper", 7 | ] 8 | resolver = "2" 9 | 10 | [workspace.dependencies] 11 | env_logger = "0.10.1" 12 | libm = "0.2.8" 13 | log = "=0.4.22" 14 | serde = { version = "1.0.217", features = ["derive"] } 15 | serde_json = "1.0.138" 16 | test-log = { version = "0.2.14", features = ["log"] } 17 | wat = "1.0.83" 18 | 19 | 20 | [package] 21 | name = "wasm-interpreter" 22 | version = "0.2.0" 23 | edition = "2021" 24 | rust-version = "1.76.0" # Keep this in sync with the requirements! 25 | description = """ 26 | A WASM interpreter tailored for safety use-cases, such as automotive and avionics applications 27 | """ 28 | homepage = "https://github.com/DLR-FT/wasm-interpreter" 29 | license = "MIT OR Apache-2.0" 30 | 31 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 32 | 33 | [lib] 34 | name = "wasm" 35 | path = "src/lib.rs" 36 | 37 | [dependencies] 38 | libm = "0.2.8" 39 | log_wrapper.path = "crates/log_wrapper" 40 | 41 | [dev-dependencies] 42 | bumpalo = "3.17.0" 43 | env_logger = { workspace = true } 44 | hexf = "0.2.1" 45 | itertools = "0.14.0" 46 | serde = { workspace = true, features = ["derive"] } 47 | serde_json = { workspace = true } 48 | test-log = { workspace = true, features = ["log"] } 49 | wast = "212.0.0" 50 | wat = { workspace = true } 51 | regex = "1.11.1" 52 | envconfig = "0.11.0" 53 | lazy_static = "1.5.0" 54 | thiserror = "2.0" 55 | log.workspace = true 56 | 57 | [features] 58 | default = ["log_wrapper/log"] 59 | -------------------------------------------------------------------------------- /src/validation/globals.rs: -------------------------------------------------------------------------------- 1 | use alloc::collections::btree_set::BTreeSet; 2 | use alloc::vec::Vec; 3 | 4 | use crate::core::indices::FuncIdx; 5 | use crate::core::reader::section_header::{SectionHeader, SectionTy}; 6 | use crate::core::reader::types::global::{Global, GlobalType}; 7 | use crate::core::reader::{WasmReadable, WasmReader}; 8 | use crate::read_constant_expression::read_constant_expression; 9 | use crate::validation_stack::ValidationStack; 10 | use crate::ValidationError; 11 | 12 | /// Validate the global section. 13 | /// 14 | /// The global section is a vector of global variables. Each [Global] variable is composed of a [GlobalType] and an 15 | /// initialization expression represented by a constant expression. 16 | /// 17 | /// See [`read_constant_expression`] for more information. 18 | pub(super) fn validate_global_section( 19 | wasm: &mut WasmReader, 20 | section_header: SectionHeader, 21 | imported_global_types: &[GlobalType], 22 | validation_context_refs: &mut BTreeSet, 23 | num_funcs: usize, 24 | ) -> Result, ValidationError> { 25 | assert_eq!(section_header.ty, SectionTy::Global); 26 | 27 | wasm.read_vec(|wasm| { 28 | let ty = GlobalType::read(wasm)?; 29 | let stack = &mut ValidationStack::new(); 30 | let (init_expr, seen_func_idxs) = 31 | read_constant_expression(wasm, stack, imported_global_types, num_funcs)?; 32 | 33 | stack.assert_val_types(&[ty.ty], true)?; 34 | validation_context_refs.extend(seen_func_idxs); 35 | 36 | Ok(Global { ty, init_expr }) 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/no-nix-ci.yaml: -------------------------------------------------------------------------------- 1 | name: No-Nix based CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "gh-readonly-queue/**" 7 | - "gh-pages" 8 | merge_group: 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | runs-on: ${{ matrix.os }} 16 | env: 17 | # for CI we can treat warnings as errors 18 | # for reference see: https://doc.rust-lang.org/clippy/usage.html 19 | RUSTFLAGS: "-D warnings" 20 | RUSTDOCFLAGS: "-D warnings" 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | submodules: true 26 | - name: Versions 27 | run: cargo --version && rustc --version 28 | - name: Format 29 | run: cargo check 30 | - name: Run clippy 31 | run: cargo clippy 32 | - name: Build docs 33 | run: cargo doc --document-private-items --verbose 34 | - name: Build 35 | run: cargo build --verbose 36 | - name: Run tests 37 | run: cargo test --verbose -- --nocapture 38 | 39 | conventional-commit-check: 40 | name: Conventional Commits 41 | runs-on: ubuntu-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | with: 45 | submodules: true 46 | - uses: webiny/action-conventional-commits@v1.3.0 47 | 48 | msrv-check: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | with: 53 | submodules: true 54 | - uses: taiki-e/install-action@cargo-hack 55 | - run: cargo hack check --rust-version --workspace --all-targets --ignore-private --ignore-rust-version 56 | -------------------------------------------------------------------------------- /tests/specification/files.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::fs::Metadata; 3 | use std::path::{Path, PathBuf}; 4 | 5 | pub fn find_wast_files( 6 | base_path: &Path, 7 | mut file_name_filter: impl FnMut(&str) -> bool, 8 | ) -> std::io::Result> { 9 | find_files_filtered(base_path, |path, meta| { 10 | let Some(file_name) = path.file_name().and_then(OsStr::to_str) else { 11 | return false; 12 | }; 13 | 14 | let if_file_then_wast_extension = 15 | meta.is_file() && path.extension() == Some(OsStr::new("wast")); 16 | 17 | file_name_filter(file_name) && if_file_then_wast_extension 18 | }) 19 | } 20 | 21 | /// Simple non-recursive depth-first directory traversal 22 | fn find_files_filtered( 23 | base_path: &Path, 24 | mut filter: impl FnMut(&Path, &Metadata) -> bool, 25 | ) -> std::io::Result> { 26 | let mut paths = vec![]; 27 | 28 | // The stack containing all directories we still have to traverse. At first contains only the base directory. 29 | let mut read_dirs = vec![std::fs::read_dir(base_path)?]; 30 | 31 | while let Some(last_read_dir) = read_dirs.last_mut() { 32 | let Some(entry) = last_read_dir.next() else { 33 | read_dirs.pop(); 34 | continue; 35 | }; 36 | 37 | let entry = entry?; 38 | let meta = entry.metadata()?; 39 | let path = entry.path(); 40 | 41 | if filter(&path, &meta) { 42 | if meta.is_file() { 43 | paths.push(entry.path()) 44 | } 45 | 46 | if meta.is_dir() { 47 | read_dirs.push(std::fs::read_dir(path)?); 48 | } 49 | } 50 | } 51 | 52 | Ok(paths) 53 | } 54 | -------------------------------------------------------------------------------- /src/core/reader/section_header.rs: -------------------------------------------------------------------------------- 1 | use crate::core::reader::span::Span; 2 | use crate::core::reader::{WasmReadable, WasmReader}; 3 | use crate::ValidationError; 4 | 5 | #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] 6 | pub enum SectionTy { 7 | Custom = 0, 8 | Type = 1, 9 | Import = 2, 10 | Function = 3, 11 | Table = 4, 12 | Memory = 5, 13 | Global = 6, 14 | Export = 7, 15 | Start = 8, 16 | Element = 9, 17 | Code = 10, 18 | Data = 11, 19 | DataCount = 12, 20 | } 21 | 22 | impl WasmReadable for SectionTy { 23 | fn read(wasm: &mut WasmReader) -> Result { 24 | use SectionTy::*; 25 | let ty = match wasm.read_u8()? { 26 | 0 => Custom, 27 | 1 => Type, 28 | 2 => Import, 29 | 3 => Function, 30 | 4 => Table, 31 | 5 => Memory, 32 | 6 => Global, 33 | 7 => Export, 34 | 8 => Start, 35 | 9 => Element, 36 | 10 => Code, 37 | 11 => Data, 38 | 12 => DataCount, 39 | other => return Err(ValidationError::MalformedSectionTypeDiscriminator(other)), 40 | }; 41 | 42 | Ok(ty) 43 | } 44 | } 45 | 46 | #[derive(Debug)] 47 | pub(crate) struct SectionHeader { 48 | pub ty: SectionTy, 49 | pub contents: Span, 50 | } 51 | 52 | impl WasmReadable for SectionHeader { 53 | fn read(wasm: &mut WasmReader) -> Result { 54 | let ty = SectionTy::read(wasm)?; 55 | let size: u32 = wasm.read_var_u32()?; 56 | let contents_span = wasm.make_span(size as usize)?; 57 | 58 | Ok(SectionHeader { 59 | ty, 60 | contents: contents_span, 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/user_data.rs: -------------------------------------------------------------------------------- 1 | use std::sync::mpsc::Sender; 2 | 3 | use wasm::{config::Config, HaltExecutionError, Store, Value}; 4 | 5 | #[test_log::test] 6 | fn counter() { 7 | #[derive(Debug, PartialEq)] 8 | struct MyCounter(pub u32); 9 | impl Config for MyCounter {} 10 | 11 | fn add_one( 12 | user_data: &mut MyCounter, 13 | _params: Vec, 14 | ) -> Result, HaltExecutionError> { 15 | user_data.0 += 1; 16 | 17 | Ok(Vec::new()) 18 | } 19 | 20 | let mut store = Store::new(MyCounter(0)); 21 | let add_one = store.func_alloc_typed_unchecked::<(), ()>(add_one); 22 | 23 | for _ in 0..5 { 24 | store 25 | .invoke_typed_without_fuel_unchecked::<(), ()>(add_one, ()) 26 | .unwrap(); 27 | } 28 | 29 | assert_eq!(store.user_data, MyCounter(5)); 30 | } 31 | 32 | #[test_log::test] 33 | fn channels() { 34 | struct MySender(pub Sender); 35 | impl Config for MySender {} 36 | 37 | let (tx, rx) = std::sync::mpsc::channel::(); 38 | 39 | std::thread::spawn(|| { 40 | fn send_message( 41 | user_data: &mut MySender, 42 | _params: Vec, 43 | ) -> Result, HaltExecutionError> { 44 | user_data 45 | .0 46 | .send("Hello from host function!".to_owned()) 47 | .unwrap(); 48 | 49 | Ok(Vec::new()) 50 | } 51 | 52 | let mut store = Store::new(MySender(tx)); 53 | let send_message = store.func_alloc_typed_unchecked::<(), ()>(send_message); 54 | 55 | store 56 | .invoke_typed_without_fuel_unchecked::<(), ()>(send_message, ()) 57 | .unwrap(); 58 | }); 59 | 60 | assert_eq!(rx.recv(), Ok("Hello from host function!".to_owned())); 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/pages_deploy_main.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation Deploy 2 | 3 | on: 4 | # When a PR is merged (or force push to main) 5 | push: 6 | branches: ["main"] 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | # Enable cargo logs to be in colour 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 15 | permissions: 16 | contents: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | build-reports: 26 | name: Build Reports using Nix 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | with: 34 | submodules: true 35 | - uses: cachix/install-nix-action@v31 36 | with: 37 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 38 | - uses: cachix/cachix-action@v16 39 | with: 40 | name: dlr-ft 41 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 42 | - run: nix build .?submodules=1#wasm-interpreter --print-build-logs 43 | - run: nix build .?submodules=1#report --print-build-logs 44 | - name: Deploy to Github Pages 45 | uses: peaceiris/actions-gh-pages@v4.0.0 46 | with: 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | # Build output to publish to the `gh-pages` branch: 49 | publish_dir: ./result 50 | publish_branch: gh-pages 51 | destination_dir: ./main/ 52 | -------------------------------------------------------------------------------- /src/execution/mod.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | 3 | use const_interpreter_loop::run_const_span; 4 | use store::HaltExecutionError; 5 | use value_stack::Stack; 6 | 7 | use crate::execution::assert_validated::UnwrapValidatedExt; 8 | use crate::execution::value::Value; 9 | use crate::interop::InteropValueList; 10 | 11 | pub(crate) mod assert_validated; 12 | pub mod checked; 13 | pub mod config; 14 | pub mod const_interpreter_loop; 15 | pub mod error; 16 | pub mod interop; 17 | mod interpreter_loop; 18 | pub mod linker; 19 | pub(crate) mod little_endian; 20 | pub mod resumable; 21 | pub mod store; 22 | pub mod value; 23 | pub mod value_stack; 24 | 25 | /// Helper function to quickly construct host functions without worrying about wasm to Rust 26 | /// type conversion. For reading/writing user data into the current configuration, simply move 27 | /// `user_data` into the passed closure. 28 | /// # Example 29 | /// ``` 30 | /// use wasm::{validate, Store, host_function_wrapper, Value, HaltExecutionError}; 31 | /// fn my_wrapped_host_func(user_data: &mut (), params: Vec) -> Result, HaltExecutionError> { 32 | /// host_function_wrapper(params, |(x, y): (u32, i32)| -> Result { 33 | /// let _user_data = user_data; 34 | /// Ok(x + (y as u32)) 35 | /// }) 36 | /// } 37 | /// fn main() { 38 | /// let mut store = Store::new(()); 39 | /// let foo_bar = store.func_alloc_typed_unchecked::<(u32, i32), u32>(my_wrapped_host_func); 40 | /// } 41 | /// ``` 42 | pub fn host_function_wrapper( 43 | params: Vec, 44 | f: impl FnOnce(Params) -> Result, 45 | ) -> Result, HaltExecutionError> { 46 | let params = 47 | Params::try_from_values(params.into_iter()).expect("Params match the actual parameters"); 48 | f(params).map(Results::into_values) 49 | } 50 | -------------------------------------------------------------------------------- /tests/arithmetic/subtraction.rs: -------------------------------------------------------------------------------- 1 | use wasm::{validate, Store}; 2 | 3 | const WAT_SUBTRACT_TEMPLATE: &str = r#" 4 | (module 5 | (func (export "subtract") (param $x {{TYPE}}) (param $y {{TYPE}}) (result {{TYPE}}) 6 | local.get $x 7 | local.get $y 8 | {{TYPE}}.sub 9 | ) 10 | ) 11 | "#; 12 | 13 | /// A simple function to multiply by 3 a i64 value and return the result 14 | #[test_log::test] 15 | pub fn i64_subtract() { 16 | let wat = String::from(WAT_SUBTRACT_TEMPLATE).replace("{{TYPE}}", "i64"); 17 | 18 | let wasm_bytes = wat::parse_str(wat).unwrap(); 19 | 20 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 21 | 22 | let mut store = Store::new(()); 23 | let module = store 24 | .module_instantiate(&validation_info, Vec::new(), None) 25 | .unwrap() 26 | .module_addr; 27 | 28 | let subtract = store 29 | .instance_export(module, "subtract") 30 | .unwrap() 31 | .as_func() 32 | .unwrap(); 33 | 34 | assert_eq!( 35 | -10_i64, 36 | store 37 | .invoke_typed_without_fuel(subtract, (1_i64, 11_i64)) 38 | .unwrap() 39 | ); 40 | assert_eq!( 41 | 0_i64, 42 | store 43 | .invoke_typed_without_fuel(subtract, (0_i64, 0_i64)) 44 | .unwrap() 45 | ); 46 | assert_eq!( 47 | 10_i64, 48 | store 49 | .invoke_typed_without_fuel(subtract, (-10_i64, -20_i64)) 50 | .unwrap() 51 | ); 52 | 53 | assert_eq!( 54 | i64::MAX - 1, 55 | store 56 | .invoke_typed_without_fuel(subtract, (i64::MAX - 1, 0_i64)) 57 | .unwrap() 58 | ); 59 | assert_eq!( 60 | i64::MIN + 3, 61 | store 62 | .invoke_typed_without_fuel(subtract, (i64::MIN + 3, 0_i64)) 63 | .unwrap() 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/execution/little_endian.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the definition and implementation of [`LittleEndianBytes`], a trait to 2 | //! convert values (such as integers or floats) to bytes in little endian byter order 3 | 4 | use super::value::{F32, F64}; 5 | 6 | /// This macro implements the [`LittleEndianBytes`] trait for a provided list of types. 7 | /// 8 | /// # Assumptions 9 | /// 10 | /// Each type for which this macro is executed must provide a `from_le_bytes` and `to_le_bytes` 11 | /// function. 12 | macro_rules! impl_LittleEndianBytes{ 13 | [$($type:ty),+] => { 14 | 15 | $(impl LittleEndianBytes<{ ::core::mem::size_of::<$type>() }> for $type { 16 | fn from_le_bytes(bytes: [u8; ::core::mem::size_of::<$type>()]) -> Self { 17 | Self::from_le_bytes(bytes) 18 | } 19 | 20 | fn to_le_bytes(self) -> [u8; ::core::mem::size_of::<$type>()] { 21 | self.to_le_bytes() 22 | } 23 | })+ 24 | } 25 | } 26 | 27 | /// Convert from and to the little endian byte representation of a value 28 | /// 29 | /// `N` denotes the number of bytes required for the little endian representation 30 | pub trait LittleEndianBytes { 31 | /// Convert from a byte array to Self 32 | fn from_le_bytes(bytes: [u8; N]) -> Self; 33 | 34 | /// Convert from self to a byte array 35 | fn to_le_bytes(self) -> [u8; N]; 36 | } 37 | 38 | // implements the [`LittleEndianBytes`] 39 | impl_LittleEndianBytes![i8, i16, i32, i64, i128, u8, u16, u32, u64, u128]; 40 | 41 | impl LittleEndianBytes<4> for F32 { 42 | fn from_le_bytes(bytes: [u8; 4]) -> Self { 43 | F32(f32::from_le_bytes(bytes)) 44 | } 45 | 46 | fn to_le_bytes(self) -> [u8; 4] { 47 | self.0.to_le_bytes() 48 | } 49 | } 50 | 51 | impl LittleEndianBytes<8> for F64 { 52 | fn from_le_bytes(bytes: [u8; 8]) -> Self { 53 | F64(f64::from_le_bytes(bytes)) 54 | } 55 | 56 | fn to_le_bytes(self) -> [u8; 8] { 57 | self.0.to_le_bytes() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![deny(clippy::undocumented_unsafe_blocks)] 3 | 4 | extern crate alloc; 5 | #[macro_use] 6 | extern crate log_wrapper; 7 | 8 | pub use core::error::ValidationError; 9 | pub use core::reader::types::{ 10 | export::ExportDesc, global::GlobalType, Limits, NumType, RefType, ValType, 11 | }; 12 | pub use core::rw_spinlock; 13 | pub use execution::error::{RuntimeError, TrapError}; 14 | 15 | pub use execution::store::*; 16 | pub use execution::value::Value; 17 | pub use execution::*; 18 | pub use validation::*; 19 | 20 | pub(crate) mod core; 21 | pub(crate) mod execution; 22 | pub(crate) mod validation; 23 | 24 | /// A definition for a [`Result`] using the optional [`Error`] type. 25 | pub type Result = ::core::result::Result; 26 | 27 | /// An opt-in error type useful for merging all error types of this crate into a single type. 28 | /// 29 | /// Note: This crate does not use this type in any public interfaces, making it optional for downstream users. 30 | #[derive(Debug, PartialEq, Eq)] 31 | pub enum Error { 32 | Validation(ValidationError), 33 | RuntimeError(RuntimeError), 34 | } 35 | 36 | impl From for Error { 37 | fn from(value: ValidationError) -> Self { 38 | Self::Validation(value) 39 | } 40 | } 41 | 42 | impl From for Error { 43 | fn from(value: RuntimeError) -> Self { 44 | Self::RuntimeError(value) 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod test { 50 | use crate::{Error, RuntimeError, ValidationError}; 51 | 52 | #[test] 53 | fn error_conversion_validation_error() { 54 | let validation_error = ValidationError::InvalidMagic; 55 | let error: Error = validation_error.into(); 56 | 57 | assert_eq!(error, Error::Validation(ValidationError::InvalidMagic)) 58 | } 59 | 60 | #[test] 61 | fn error_conversion_runtime_error() { 62 | let runtime_error = RuntimeError::ModuleNotFound; 63 | let error: Error = runtime_error.into(); 64 | 65 | assert_eq!(error, Error::RuntimeError(RuntimeError::ModuleNotFound)) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /tests/specification/README.md: -------------------------------------------------------------------------------- 1 | # Testsuite Runner 2 | 3 | ## Using the Testsuite Runner 4 | 5 | 1. `git submodule update --init` to fetch the testsuite submodule 6 | 1. Edit the `mod.rs` file to your liking 7 | - This means you can include and exclude files and folders as you wish (only one of these two filters can be active at any one point) 8 | - Example: 9 | 10 | ```rs 11 | #[test_log::test] 12 | pub fn spec_tests() { 13 | let filters = Filter::Exclude(FnF { 14 | folders: Some(vec!["proposals".to_string()]), // exclude any folders you want 15 | files: Some(vec!["custom.wast".to_string()]) // files, as well 16 | }); 17 | 18 | let filters = Filter::Include(FnF { 19 | folders: Some(vec!["proposals".to_string()]), // include only folders you want 20 | }); 21 | 22 | // then get the paths of the files and you are good to go 23 | let paths = get_wast_files(Path::new("./tests/specification/testsuite/"), &filters) 24 | .expect("Failed to find testsuite"); 25 | 26 | 27 | // or you can just do this for one test 28 | let pb: PathBuf = "./tests/specification/testsuite/table_grow.wast".into(); 29 | let mut paths = Vec::new(); 30 | paths.push(pb); 31 | 32 | let mut successful_reports = 0; 33 | let mut failed_reports = 0; 34 | let mut compile_error_reports = 0; 35 | 36 | for test_path in paths { 37 | println!("Report for {}:", test_path.display()); 38 | let report = run::run_spec_test(test_path.to_str().unwrap()); 39 | match report { 40 | reports::WastTestReport::Asserts(assert_report) => { 41 | if assert_report.has_errors() { 42 | failed_reports += 1; 43 | } else { 44 | successful_reports += 1; 45 | } 46 | } 47 | reports::WastTestReport::CompilationError(_) => { 48 | compile_error_reports += 1; 49 | } 50 | } 51 | } 52 | 53 | println!( 54 | "Tests: {} Passed, {} Failed, {} Compilation Errors", 55 | successful_reports, failed_reports, compile_error_reports 56 | ); 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /tests/add_one.rs: -------------------------------------------------------------------------------- 1 | use wasm::{validate, Store}; 2 | 3 | const MULTIPLY_WAT_TEMPLATE: &str = r#" 4 | (module 5 | (func (export "add_one") (param $x {{TYPE}}) (result {{TYPE}}) 6 | local.get $x 7 | {{TYPE}}.const 1 8 | {{TYPE}}.add) 9 | ) 10 | "#; 11 | 12 | /// A simple function to add 1 to an i32 and return the result 13 | #[test_log::test] 14 | fn i32_add_one() { 15 | let wat = String::from(MULTIPLY_WAT_TEMPLATE).replace("{{TYPE}}", "i32"); 16 | let wasm_bytes = wat::parse_str(wat).unwrap(); 17 | 18 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 19 | let mut store = Store::new(()); 20 | let module = store 21 | .module_instantiate(&validation_info, Vec::new(), None) 22 | .unwrap() 23 | .module_addr; 24 | 25 | let add_one = store 26 | .instance_export(module, "add_one") 27 | .unwrap() 28 | .as_func() 29 | .unwrap(); 30 | 31 | assert_eq!(12, store.invoke_typed_without_fuel(add_one, 11).unwrap()); 32 | assert_eq!(1, store.invoke_typed_without_fuel(add_one, 0).unwrap()); 33 | assert_eq!(-5, store.invoke_typed_without_fuel(add_one, -6).unwrap()); 34 | } 35 | 36 | /// A simple function to add 1 to an i64 and return the result 37 | #[test_log::test] 38 | fn i64_add_one() { 39 | let wat = String::from(MULTIPLY_WAT_TEMPLATE).replace("{{TYPE}}", "i64"); 40 | let wasm_bytes = wat::parse_str(wat).unwrap(); 41 | 42 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 43 | let mut store = Store::new(()); 44 | let module = store 45 | .module_instantiate(&validation_info, Vec::new(), None) 46 | .unwrap() 47 | .module_addr; 48 | 49 | let add_one = store 50 | .instance_export(module, "add_one") 51 | .unwrap() 52 | .as_func() 53 | .unwrap(); 54 | 55 | assert_eq!( 56 | 12_i64, 57 | store.invoke_typed_without_fuel(add_one, 11_i64).unwrap() 58 | ); 59 | assert_eq!( 60 | 1_i64, 61 | store.invoke_typed_without_fuel(add_one, 0_i64).unwrap() 62 | ); 63 | assert_eq!( 64 | -5_i64, 65 | store.invoke_typed_without_fuel(add_one, -6_i64).unwrap() 66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /tests/structured_control_flow/loop.rs: -------------------------------------------------------------------------------- 1 | use wasm::{validate, Store}; 2 | 3 | const FIBONACCI_WITH_LOOP_AND_BR_IF: &str = r#" 4 | (module 5 | (func (export "fibonacci") (param $n i32) (result i32) 6 | (local $prev i32) 7 | (local $curr i32) 8 | (local $counter i32) 9 | 10 | i32.const 0 11 | local.set $prev 12 | i32.const 1 13 | local.set $curr 14 | 15 | local.get $n 16 | i32.const 1 17 | i32.add 18 | local.set $counter 19 | 20 | block $exit 21 | loop $loop 22 | local.get $counter 23 | i32.const 1 24 | i32.le_s 25 | br_if $exit 26 | 27 | local.get $curr 28 | local.get $curr 29 | local.get $prev 30 | i32.add 31 | local.set $curr 32 | local.set $prev 33 | 34 | local.get $counter 35 | i32.const 1 36 | i32.sub 37 | local.set $counter 38 | 39 | br $loop 40 | 41 | drop 42 | drop 43 | drop 44 | 45 | end $loop 46 | end $exit 47 | 48 | local.get $curr 49 | ) 50 | )"#; 51 | 52 | #[test_log::test] 53 | fn fibonacci_with_loop_and_br_if() { 54 | let wasm_bytes = wat::parse_str(FIBONACCI_WITH_LOOP_AND_BR_IF).unwrap(); 55 | 56 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 57 | let mut store = Store::new(()); 58 | let module = store 59 | .module_instantiate(&validation_info, Vec::new(), None) 60 | .unwrap() 61 | .module_addr; 62 | 63 | let fibonacci_fn = store 64 | .instance_export(module, "fibonacci") 65 | .unwrap() 66 | .as_func() 67 | .unwrap(); 68 | 69 | assert_eq!( 70 | 1, 71 | store.invoke_typed_without_fuel(fibonacci_fn, -5).unwrap() 72 | ); 73 | assert_eq!(1, store.invoke_typed_without_fuel(fibonacci_fn, 0).unwrap()); 74 | assert_eq!(1, store.invoke_typed_without_fuel(fibonacci_fn, 1).unwrap()); 75 | assert_eq!(2, store.invoke_typed_without_fuel(fibonacci_fn, 2).unwrap()); 76 | assert_eq!(3, store.invoke_typed_without_fuel(fibonacci_fn, 3).unwrap()); 77 | assert_eq!(5, store.invoke_typed_without_fuel(fibonacci_fn, 4).unwrap()); 78 | assert_eq!(8, store.invoke_typed_without_fuel(fibonacci_fn, 5).unwrap()); 79 | } 80 | -------------------------------------------------------------------------------- /tests/rw_spinlock.rs: -------------------------------------------------------------------------------- 1 | use std::{thread, time::Duration}; 2 | 3 | use log::info; 4 | use wasm::rw_spinlock::*; 5 | 6 | #[test_log::test] 7 | fn rw_spin_lock_basic() { 8 | let rw_lock = RwSpinLock::new(0i128); 9 | 10 | // one reader 11 | { 12 | let reader = rw_lock.read(); 13 | assert_eq!(*reader, 0); 14 | } 15 | 16 | // one writer 17 | { 18 | let mut writer = rw_lock.write(); 19 | *writer = 13; 20 | } 21 | 22 | // one writer used for reading 23 | { 24 | let writer = rw_lock.write(); 25 | assert_eq!(*writer, 13); 26 | } 27 | 28 | // 10_000 readers 29 | { 30 | let mut vec = Vec::new(); 31 | for _ in 0..10_000 { 32 | vec.push(rw_lock.read()); 33 | } 34 | } 35 | 36 | // a final writer 37 | assert_eq!(*rw_lock.write(), 13); 38 | } 39 | 40 | #[test_log::test] 41 | fn write_dominates_read() { 42 | let rw_lock = RwSpinLock::new(0u128); 43 | 44 | thread::scope(|s| { 45 | // this thread ensures that there first is a read lock 46 | s.spawn(|| { 47 | info!("t1: acquiring read lock"); 48 | let read_guard = rw_lock.read(); 49 | 50 | info!("t1: waiting"); 51 | thread::sleep(Duration::from_millis(500)); 52 | 53 | info!("t1: reading once"); 54 | assert_eq!(*read_guard, 0u128); 55 | info!("t1: terminating"); 56 | }); 57 | 58 | for _ in 0..64 { 59 | s.spawn(|| { 60 | thread::sleep(Duration::from_millis(250)); 61 | 62 | loop { 63 | info!("tx: acquiring readlock"); 64 | let read_guard = rw_lock.read(); 65 | thread::sleep(Duration::from_millis(500)); 66 | if *read_guard != 0 { 67 | return; 68 | } 69 | } 70 | }); 71 | } 72 | 73 | s.spawn(|| { 74 | thread::sleep(Duration::from_millis(750)); 75 | 76 | info!("t2: acquiring write lock"); 77 | let mut write_guard = rw_lock.write(); 78 | 79 | *write_guard = 1u128 << 74; 80 | }); 81 | }); 82 | 83 | assert_eq!(*rw_lock.read(), 1u128 << 74); 84 | } 85 | -------------------------------------------------------------------------------- /tests/memory_fill.rs: -------------------------------------------------------------------------------- 1 | /* 2 | # This file incorporates code from the WebAssembly testsuite, originally 3 | # available at https://github.com/WebAssembly/testsuite. 4 | # 5 | # The original code is licensed under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | */ 17 | 18 | // use core::slice::SlicePattern; 19 | 20 | use wasm::{validate, Store}; 21 | 22 | #[test_log::test] 23 | fn memory_fill() { 24 | let w = r#" 25 | (module 26 | (memory (export "mem") 1) 27 | (func (export "fill") 28 | (memory.fill (i32.const 0) (i32.const 2777) (i32.const 100)) 29 | ) 30 | ) 31 | "#; 32 | let wasm_bytes = wat::parse_str(w).unwrap(); 33 | let validation_info = validate(&wasm_bytes).unwrap(); 34 | let mut store = Store::new(()); 35 | let module = store 36 | .module_instantiate(&validation_info, Vec::new(), None) 37 | .unwrap() 38 | .module_addr; 39 | 40 | let fill = store 41 | .instance_export(module, "fill") 42 | .unwrap() 43 | .as_func() 44 | .unwrap(); 45 | let mem = store 46 | .instance_export(module, "mem") 47 | .unwrap() 48 | .as_mem() 49 | .expect("memory"); 50 | 51 | store.invoke_typed_without_fuel::<(), ()>(fill, ()).unwrap(); 52 | 53 | let expected = [vec![217u8; 100], vec![0u8; 5]].concat(); 54 | for (idx, expected_byte) in expected.into_iter().enumerate() { 55 | let mem_byte: u8 = store.mem_read(mem, idx as u32).unwrap(); 56 | assert_eq!( 57 | mem_byte.to_ascii_lowercase(), 58 | expected_byte.to_ascii_lowercase() 59 | ); 60 | } 61 | } 62 | 63 | // we need control flow implemented for any of these tests 64 | #[ignore = "not yet implemented"] 65 | #[test_log::test] 66 | fn memory_fill_with_control_flow() { 67 | todo!() 68 | } 69 | -------------------------------------------------------------------------------- /tests/basic_memory.rs: -------------------------------------------------------------------------------- 1 | use wasm::{validate, Store}; 2 | const BASE_WAT: &str = r#" 3 | (module 4 | (memory 1) 5 | (func (export "store_num") (param $x {{TYPE}}) 6 | ({{TYPE}}.store (i32.const 0) (local.get $x)) 7 | ) 8 | (func (export "load_num") (result {{TYPE}}) 9 | ({{TYPE}}.load (i32.const 0)) 10 | ) 11 | ) 12 | "#; 13 | /// Two simple methods for storing and loading an i32 from the first slot in linear memory. 14 | #[test_log::test] 15 | fn basic_memory() { 16 | let wat = String::from(BASE_WAT).replace("{{TYPE}}", "i32"); 17 | let wasm_bytes = wat::parse_str(wat).unwrap(); 18 | 19 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 20 | let mut store = Store::new(()); 21 | let module = store 22 | .module_instantiate(&validation_info, Vec::new(), None) 23 | .unwrap() 24 | .module_addr; 25 | 26 | let store_num = store 27 | .instance_export(module, "store_num") 28 | .unwrap() 29 | .as_func() 30 | .unwrap(); 31 | 32 | let load_num = store 33 | .instance_export(module, "load_num") 34 | .unwrap() 35 | .as_func() 36 | .unwrap(); 37 | 38 | let _ = store.invoke_typed_without_fuel::(store_num, 42); 39 | assert_eq!(42, store.invoke_typed_without_fuel(load_num, ()).unwrap()); 40 | } 41 | 42 | /// Two simple methods for storing and loading an f32 from the first slot in linear memory. 43 | #[test_log::test] 44 | fn f32_basic_memory() { 45 | let wat = String::from(BASE_WAT).replace("{{TYPE}}", "f32"); 46 | let wasm_bytes = wat::parse_str(wat).unwrap(); 47 | 48 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 49 | let mut store = Store::new(()); 50 | let module = store 51 | .module_instantiate(&validation_info, Vec::new(), None) 52 | .unwrap() 53 | .module_addr; 54 | 55 | let store_num = store 56 | .instance_export(module, "store_num") 57 | .unwrap() 58 | .as_func() 59 | .unwrap(); 60 | 61 | let load_num = store 62 | .instance_export(module, "load_num") 63 | .unwrap() 64 | .as_func() 65 | .unwrap(); 66 | 67 | store 68 | .invoke_typed_without_fuel::(store_num, 133.7_f32) 69 | .unwrap(); 70 | assert_eq!( 71 | 133.7_f32, 72 | store.invoke_typed_without_fuel(load_num, ()).unwrap() 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /ci-tools/start_nix.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=" 4 | echo "This script is not actively maintained by the" 5 | echo "development team. Please run at your own risk." 6 | echo "Feel free to report any issues on our github," 7 | echo "but we offer no guarantees of a fix being" 8 | echo "implemented." 9 | echo "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=" 10 | 11 | if ! grep -q WSL /proc/version && grep -q MINGW /proc/version 2>/dev/null; then 12 | echo "This script requires WSL if ran on windows for proper path handling with Docker." 13 | echo "Please run this script from within WSL instead of Git Bash." 14 | exit 1 15 | fi 16 | 17 | 18 | # Make sure that we are in the `ci-tools` directory 19 | current_dir=$(basename "$(pwd)") 20 | if [ "$current_dir" != "ci-tools" ]; then 21 | if [ -d "ci-tools" ]; then 22 | cd "ci-tools" || exit 1 23 | else 24 | echo "Error: ci-tools dir not found -- where are you running this from?" 25 | exit 1 26 | fi 27 | fi 28 | 29 | mkdir -p nix-sharedfs 30 | docker build -t nix-playground . 31 | 32 | # Copy the parent directory contents into the container's workspace. This way, any results do not backspill onto the 33 | # host which can mess up wsl/windows. We want to copy only the things git would copy (so we don't copy over `.git` or 34 | # `target` or anything else git wouldn't copy) 35 | 36 | temp_tar=$(mktemp) 37 | cd .. 38 | git archive --format=tar HEAD > "$temp_tar" 39 | cd - || exit 1 40 | 41 | CONTAINER_NAME="nix-playground-container" 42 | 43 | if ! docker container inspect "$CONTAINER_NAME" >/dev/null 2>&1; then 44 | echo "Creating new container..." 45 | 46 | docker run -dit \ 47 | --name "$CONTAINER_NAME" \ 48 | -v "$(pwd)/nix-sharedfs:/nix-sharedfs" \ 49 | --workdir /workspace \ 50 | nix-playground \ 51 | bash -c "mkdir -p /workspace && cd /workspace && bash" 52 | else 53 | echo "Container exists, updating workspace and starting..." 54 | docker start "$CONTAINER_NAME" 55 | fi 56 | 57 | docker cp "$temp_tar" "$CONTAINER_NAME:/tmp/repo.tar" 58 | docker exec "$CONTAINER_NAME" bash -c 'mkdir -p /workspace && cd /workspace && tar xf /tmp/repo.tar' 59 | docker attach "$CONTAINER_NAME" 60 | 61 | docker start "$CONTAINER_NAME" 62 | docker exec "$CONTAINER_NAME" bash -c 'rm -rf /workspace/*' 63 | docker exec "$CONTAINER_NAME" bash -c 'rm -f /tmp/repo.tar' 64 | docker stop "$CONTAINER_NAME" 65 | 66 | rm "$temp_tar" 67 | -------------------------------------------------------------------------------- /tests/specification/ci_reports.rs: -------------------------------------------------------------------------------- 1 | use super::reports::{AssertOutcome, AssertReport, ScriptError}; 2 | 3 | #[derive(serde::Serialize)] 4 | pub struct CIFullReport { 5 | pub entries: Vec, 6 | } 7 | 8 | impl CIFullReport { 9 | pub fn new(report: Vec>) -> Self { 10 | Self { 11 | entries: report.into_iter().map(CIReportHeader::new).collect(), 12 | } 13 | } 14 | } 15 | 16 | #[derive(serde::Serialize)] 17 | pub struct CIReportHeader { 18 | pub filepath: String, 19 | pub data: CIReportData, 20 | } 21 | impl CIReportHeader { 22 | fn new(report: Result) -> Self { 23 | let filepath = match &report { 24 | Ok(assert_report) => assert_report.filename.clone(), 25 | Err(script_error) => script_error.filename.clone(), 26 | }; 27 | 28 | Self { 29 | filepath, 30 | data: CIReportData::new(report), 31 | } 32 | } 33 | } 34 | 35 | #[derive(serde::Serialize)] 36 | pub enum CIReportData { 37 | Assert { 38 | results: Vec, 39 | }, 40 | ScriptError { 41 | error: String, 42 | context: String, 43 | line_number: Option, 44 | command: Option, 45 | }, 46 | } 47 | impl CIReportData { 48 | fn new(report: Result) -> Self { 49 | match report { 50 | Ok(assert_report) => Self::Assert { 51 | results: assert_report 52 | .results 53 | .into_iter() 54 | .map(CIAssert::new) 55 | .collect(), 56 | }, 57 | Err(script_error) => Self::ScriptError { 58 | error: script_error.error.to_string(), 59 | context: script_error.context.clone(), 60 | line_number: script_error.line_number, 61 | command: script_error.command.clone(), 62 | }, 63 | } 64 | } 65 | } 66 | 67 | #[derive(serde::Serialize)] 68 | pub struct CIAssert { 69 | pub error: Option, 70 | pub line_number: u32, 71 | pub command: String, 72 | } 73 | impl CIAssert { 74 | fn new(assert_outcome: AssertOutcome) -> Self { 75 | Self { 76 | line_number: assert_outcome.line_number, 77 | command: assert_outcome.command, 78 | error: assert_outcome.maybe_error.map(|err| err.to_string()), 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkgs/wasm-interpreter.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | rustPlatform, 4 | doDoc ? true, 5 | useNextest ? true, 6 | }: 7 | 8 | let 9 | cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml); 10 | in 11 | rustPlatform.buildRustPackage rec { 12 | pname = cargoToml.package.name; 13 | version = cargoToml.package.version; 14 | 15 | src = 16 | let 17 | # original source to read from 18 | src = ./..; 19 | 20 | # File suffices to include 21 | extensions = [ 22 | "lock" 23 | "rs" 24 | "toml" 25 | "wast" 26 | ]; 27 | # Files to explicitly include 28 | include = [ ]; 29 | # Files to explicitly exclude 30 | exclude = [ 31 | "flake.lock" 32 | "treefmt.toml" 33 | ]; 34 | 35 | filter = ( 36 | path: type: 37 | let 38 | inherit (builtins) baseNameOf toString; 39 | inherit (lib.lists) any; 40 | inherit (lib.strings) hasSuffix removePrefix; 41 | inherit (lib.trivial) id; 42 | 43 | # consumes a list of bools, returns true if any of them is true 44 | anyof = any id; 45 | 46 | basename = baseNameOf (toString path); 47 | relative = removePrefix (toString src + "/") (toString path); 48 | in 49 | (anyof [ 50 | (type == "directory") 51 | (any (ext: hasSuffix ".${ext}" basename) extensions) 52 | (any (file: file == relative) include) 53 | ]) 54 | && !(anyof [ (any (file: file == relative) exclude) ]) 55 | ); 56 | in 57 | lib.sources.cleanSourceWith { inherit src filter; }; 58 | 59 | cargoLock.lockFile = src + "/Cargo.lock"; 60 | 61 | # we want a full documentation, if at all 62 | postBuild = lib.strings.optionalString doDoc '' 63 | cargo doc --document-private-items 64 | mkdir -- "$out" 65 | 66 | shopt -s globstar 67 | mv -- target/**/doc "$out/" 68 | shopt -u globstar 69 | ''; 70 | 71 | # nextest can emit JUnit test reports 72 | inherit useNextest; 73 | 74 | # if using nextest, use the ci profile 75 | cargoTestFlags = lib.lists.optional useNextest "--profile=ci"; 76 | 77 | # if using nextest, it will create a junit.xml 78 | postCheck = lib.strings.optionalString useNextest '' 79 | shopt -s globstar 80 | mv -- target/**/nextest/ci/junit.xml "$out/" 2> /dev/null \ 81 | && echo 'installed junit.xml' || true 82 | shopt -u globstar 83 | ''; 84 | 85 | meta = { 86 | inherit (cargoToml.package) description homepage; 87 | license = with lib.licenses; [ 88 | asl20 89 | # OR 90 | mit 91 | ]; 92 | maintainers = [ lib.maintainers.wucke13 ]; 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /crates/compare-testsuite-rs/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context as _; 2 | use ci_reports::CIFullReport; 3 | use clap::Parser; 4 | use deltas::FileDeltas; 5 | use std::{fmt::Display, fs::File, io::BufReader, path::PathBuf}; 6 | use summary::ReportSummary; 7 | 8 | #[derive(Parser, Debug)] 9 | #[command(version, about, long_about = None)] 10 | /// Generate a Markdown report for the differences of testsuite results 11 | struct Args { 12 | /// Path to the testsuite output JSON for the old version 13 | old_results_path: PathBuf, 14 | 15 | /// Path to the testsuite output JSON for the new version 16 | new_results_path: PathBuf, 17 | } 18 | 19 | mod ci_reports; 20 | mod deltas; 21 | mod summary; 22 | 23 | fn main() -> anyhow::Result<()> { 24 | // Read arguments 25 | let args = Args::parse(); 26 | let old_file = File::open(args.old_results_path).context("failed to open old results file")?; 27 | let new_file = File::open(args.new_results_path).context("failed to open new results file")?; 28 | 29 | // Read & parse the data 30 | let old_report: CIFullReport = serde_json::from_reader(BufReader::new(old_file)) 31 | .context("failed to parse old file contents")?; 32 | let new_report: CIFullReport = serde_json::from_reader(BufReader::new(new_file)) 33 | .context("failed to parse new file contents")?; 34 | 35 | // Analyze the data 36 | let deltas = 37 | deltas::generate(&old_report, &new_report).context("failed to generate file deltas")?; 38 | let summary = summary::generate(&new_report); 39 | let report = TestsuiteReport { deltas, summary }; 40 | 41 | // Generate the final report 42 | println!("{report}"); 43 | 44 | Ok(()) 45 | } 46 | 47 | struct TestsuiteReport<'report> { 48 | deltas: FileDeltas<'report>, 49 | summary: ReportSummary<'report>, 50 | } 51 | 52 | impl Display for TestsuiteReport<'_> { 53 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 54 | writeln!( 55 | f, 56 | "# 🗒️ [WebAssembly Testsuite](https://github.com/WebAssembly/testsuite) Report" 57 | )?; 58 | writeln!(f)?; 59 | 60 | writeln!(f, "{}", self.deltas)?; 61 | writeln!(f, "{}", self.summary) 62 | } 63 | } 64 | 65 | pub fn sanitize_path(path: &str) -> &str { 66 | path.trim_start_matches("./tests/specification/testsuite/") 67 | } 68 | 69 | pub fn write_details_summary( 70 | writer: &mut W, 71 | summary: impl FnOnce(&mut W) -> std::fmt::Result, 72 | contents: impl FnOnce(&mut W) -> std::fmt::Result, 73 | ) -> std::fmt::Result { 74 | write!(writer, "
")?; 75 | summary(writer)?; 76 | write!(writer, "")?; 77 | contents(writer)?; 78 | write!(writer, "
") 79 | } 80 | -------------------------------------------------------------------------------- /tests/select.rs: -------------------------------------------------------------------------------- 1 | use wasm::{validate, Store}; 2 | 3 | const SELECT_TEST: &str = r#" 4 | (module 5 | (func (export "select_test") (param $num i32) (result i32) 6 | (if (result i32) 7 | (i32.le_s 8 | (local.get $num) 9 | (i32.const 1) 10 | ) 11 | (then 12 | (select {{TYPE_1}} 13 | (i32.const 8) 14 | (i32.const 4) 15 | (local.get $num) 16 | ) 17 | ) 18 | (else 19 | (i32.wrap_i64 20 | (select {{TYPE_2}} 21 | (i64.const 16) 22 | (i64.const 15) 23 | (i32.sub (local.get $num) (i32.const 2)) 24 | ) 25 | ) 26 | ) 27 | ) 28 | ) 29 | )"#; 30 | 31 | #[test_log::test] 32 | fn polymorphic_select_test() { 33 | let wat = String::from(SELECT_TEST) 34 | .replace("{{TYPE_1}}", "") 35 | .replace("{{TYPE_2}}", ""); 36 | let wasm_bytes = wat::parse_str(wat).unwrap(); 37 | validate(&wasm_bytes).expect("validation failed"); 38 | 39 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 40 | let mut store = Store::new(()); 41 | let module = store 42 | .module_instantiate(&validation_info, Vec::new(), None) 43 | .unwrap() 44 | .module_addr; 45 | 46 | let select_test = store 47 | .instance_export(module, "select_test") 48 | .unwrap() 49 | .as_func() 50 | .unwrap(); 51 | 52 | assert_eq!(4, store.invoke_typed_without_fuel(select_test, 0).unwrap()); 53 | assert_eq!(8, store.invoke_typed_without_fuel(select_test, 1).unwrap()); 54 | assert_eq!(15, store.invoke_typed_without_fuel(select_test, 2).unwrap()); 55 | assert_eq!(16, store.invoke_typed_without_fuel(select_test, 3).unwrap()); 56 | } 57 | 58 | #[test_log::test] 59 | fn typed_select_test() { 60 | let wat = String::from(SELECT_TEST) 61 | .replace("{{TYPE_1}}", "(result i32)") 62 | .replace("{{TYPE_2}}", "(result i64)"); 63 | let wasm_bytes = wat::parse_str(wat).unwrap(); 64 | validate(&wasm_bytes).expect("validation failed"); 65 | 66 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 67 | let mut store = Store::new(()); 68 | let module = store 69 | .module_instantiate(&validation_info, Vec::new(), None) 70 | .unwrap() 71 | .module_addr; 72 | 73 | let select_test = store 74 | .instance_export(module, "select_test") 75 | .unwrap() 76 | .as_func() 77 | .unwrap(); 78 | 79 | assert_eq!(4, store.invoke_typed_without_fuel(select_test, 0).unwrap()); 80 | assert_eq!(8, store.invoke_typed_without_fuel(select_test, 1).unwrap()); 81 | assert_eq!(15, store.invoke_typed_without_fuel(select_test, 2).unwrap()); 82 | assert_eq!(16, store.invoke_typed_without_fuel(select_test, 3).unwrap()); 83 | } 84 | -------------------------------------------------------------------------------- /.github/workflows/testsuite_preview.yaml: -------------------------------------------------------------------------------- 1 | name: Testsuite Preview 2 | 3 | on: 4 | - pull_request_target 5 | 6 | permissions: 7 | pull-requests: write 8 | contents: read 9 | 10 | jobs: 11 | generate-base-report: 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | runs-on: ${{ matrix.os }} 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | ref: ${{ github.event.pull_request.base.ref }} 22 | submodules: true 23 | repository: ${{ github.event.pull_request.base.repo.full_name }} 24 | 25 | - name: Generate old report 26 | run: | 27 | TESTSUITE_SAVE=1 cargo test -- spec_tests --show-output 28 | cp testsuite_results.json old.json || : 29 | 30 | - name: Upload Base Report 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: base-report 34 | path: old.json 35 | retention-days: 1 36 | 37 | - run: cargo clean 38 | 39 | generate-head-report: 40 | strategy: 41 | matrix: 42 | os: [ubuntu-latest] 43 | runs-on: ${{ matrix.os }} 44 | env: 45 | RUSTFLAGS: "-D warnings" 46 | RUSTDOCFLAGS: "-D warnings" 47 | 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | fetch-depth: 0 52 | ref: ${{ github.event.pull_request.head.ref }} 53 | submodules: true 54 | repository: ${{ github.event.pull_request.head.repo.full_name }} 55 | 56 | - name: Generate new report 57 | run: | 58 | TESTSUITE_SAVE=1 cargo test -- spec_tests --show-output 59 | cp testsuite_results.json new.json || : 60 | 61 | - name: Upload Head Report 62 | uses: actions/upload-artifact@v4 63 | with: 64 | name: head-report 65 | path: new.json 66 | retention-days: 1 67 | 68 | - run: cargo clean 69 | 70 | compare-reports: 71 | needs: [generate-base-report, generate-head-report] 72 | strategy: 73 | matrix: 74 | os: [ubuntu-latest] 75 | runs-on: ${{ matrix.os }} 76 | 77 | steps: 78 | - uses: actions/checkout@v4 79 | 80 | - name: Download Base Report 81 | continue-on-error: true 82 | uses: actions/download-artifact@v4 83 | with: 84 | name: base-report 85 | 86 | - name: Download Head Report 87 | continue-on-error: true 88 | uses: actions/download-artifact@v4 89 | with: 90 | name: head-report 91 | 92 | - name: Compare reports 93 | run: cargo run --package=compare-testsuite-rs -- old.json new.json > testsuite_report.md 94 | 95 | - name: Sticky Pull Request Comment 96 | uses: marocchino/sticky-pull-request-comment@v2.9.1 97 | with: 98 | header: testsuite 99 | path: testsuite_report.md 100 | -------------------------------------------------------------------------------- /tests/arithmetic/multiply.rs: -------------------------------------------------------------------------------- 1 | use wasm::{validate, Store}; 2 | 3 | const MULTIPLY_WAT_TEMPLATE: &str = r#" 4 | (module 5 | (func (export "multiply") (param $x {{TYPE}}) (result {{TYPE}}) 6 | local.get $x 7 | {{TYPE}}.const 3 8 | {{TYPE}}.mul 9 | ) 10 | ) 11 | "#; 12 | /// A simple function to multiply by 3 a i32 value and return the result 13 | #[test_log::test] 14 | pub fn i32_multiply() { 15 | let wat = String::from(MULTIPLY_WAT_TEMPLATE).replace("{{TYPE}}", "i32"); 16 | 17 | let wasm_bytes = wat::parse_str(wat).unwrap(); 18 | 19 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 20 | 21 | let mut store = Store::new(()); 22 | let module = store 23 | .module_instantiate(&validation_info, Vec::new(), None) 24 | .unwrap() 25 | .module_addr; 26 | 27 | let multiply = store 28 | .instance_export(module, "multiply") 29 | .unwrap() 30 | .as_func() 31 | .unwrap(); 32 | 33 | assert_eq!(33, store.invoke_typed_without_fuel(multiply, 11).unwrap()); 34 | assert_eq!(0, store.invoke_typed_without_fuel(multiply, 0).unwrap()); 35 | assert_eq!(-30, store.invoke_typed_without_fuel(multiply, -10).unwrap()); 36 | 37 | assert_eq!( 38 | i32::MAX - 5, 39 | store 40 | .invoke_typed_without_fuel(multiply, i32::MAX - 1) 41 | .unwrap() 42 | ); 43 | assert_eq!( 44 | i32::MIN + 3, 45 | store 46 | .invoke_typed_without_fuel(multiply, i32::MIN + 1) 47 | .unwrap() 48 | ); 49 | } 50 | 51 | /// A simple function to multiply by 3 a i64 value and return the result 52 | #[test_log::test] 53 | pub fn i64_multiply() { 54 | let wat = String::from(MULTIPLY_WAT_TEMPLATE).replace("{{TYPE}}", "i64"); 55 | 56 | let wasm_bytes = wat::parse_str(wat).unwrap(); 57 | 58 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 59 | 60 | let mut store = Store::new(()); 61 | let module = store 62 | .module_instantiate(&validation_info, Vec::new(), None) 63 | .unwrap() 64 | .module_addr; 65 | 66 | let multiply = store 67 | .instance_export(module, "multiply") 68 | .unwrap() 69 | .as_func() 70 | .unwrap(); 71 | 72 | assert_eq!( 73 | 33_i64, 74 | store.invoke_typed_without_fuel(multiply, 11_i64).unwrap() 75 | ); 76 | assert_eq!( 77 | 0_i64, 78 | store.invoke_typed_without_fuel(multiply, 0_i64).unwrap() 79 | ); 80 | assert_eq!( 81 | -30_i64, 82 | store.invoke_typed_without_fuel(multiply, -10_i64).unwrap() 83 | ); 84 | 85 | assert_eq!( 86 | i64::MAX - 5, 87 | store 88 | .invoke_typed_without_fuel(multiply, i64::MAX - 1) 89 | .unwrap() 90 | ); 91 | assert_eq!( 92 | i64::MIN + 3, 93 | store 94 | .invoke_typed_without_fuel(multiply, i64::MIN + 1) 95 | .unwrap() 96 | ); 97 | } 98 | -------------------------------------------------------------------------------- /src/core/reader/types/import.rs: -------------------------------------------------------------------------------- 1 | use alloc::borrow::ToOwned; 2 | use alloc::string::String; 3 | 4 | use crate::core::indices::TypeIdx; 5 | use crate::core::reader::{WasmReadable, WasmReader}; 6 | use crate::{ValidationError, ValidationInfo}; 7 | 8 | use super::global::GlobalType; 9 | use super::{ExternType, MemType, TableType}; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct Import { 13 | #[allow(warnings)] 14 | pub module_name: String, 15 | #[allow(warnings)] 16 | pub name: String, 17 | #[allow(warnings)] 18 | pub desc: ImportDesc, 19 | } 20 | 21 | impl WasmReadable for Import { 22 | fn read(wasm: &mut WasmReader) -> Result { 23 | let module_name = wasm.read_name()?.to_owned(); 24 | let name = wasm.read_name()?.to_owned(); 25 | let desc = ImportDesc::read(wasm)?; 26 | 27 | Ok(Self { 28 | module_name, 29 | name, 30 | desc, 31 | }) 32 | } 33 | } 34 | 35 | #[derive(Debug, Clone)] 36 | pub enum ImportDesc { 37 | Func(TypeIdx), 38 | Table(TableType), 39 | Mem(MemType), 40 | Global(GlobalType), 41 | } 42 | 43 | impl WasmReadable for ImportDesc { 44 | fn read(wasm: &mut WasmReader) -> Result { 45 | let desc = match wasm.read_u8()? { 46 | 0x00 => Self::Func(wasm.read_var_u32()? as TypeIdx), 47 | // https://webassembly.github.io/spec/core/binary/types.html#table-types 48 | 0x01 => Self::Table(TableType::read(wasm)?), 49 | 0x02 => Self::Mem(MemType::read(wasm)?), 50 | 0x03 => Self::Global(GlobalType::read(wasm)?), 51 | other => return Err(ValidationError::MalformedImportDescDiscriminator(other)), 52 | }; 53 | 54 | Ok(desc) 55 | } 56 | } 57 | 58 | impl ImportDesc { 59 | /// returns the external type of `self` according to typing relation, 60 | /// taking `validation_info` as validation context C 61 | /// 62 | /// Note: This method may panic if self does not come from the given [`ValidationInfo`]. 63 | /// 64 | pub fn extern_type(&self, validation_info: &ValidationInfo) -> ExternType { 65 | match self { 66 | ImportDesc::Func(type_idx) => { 67 | // unlike ExportDescs, these directly refer to the types section 68 | // since a corresponding function entry in function section or body 69 | // in code section does not exist for these 70 | let func_type = validation_info 71 | .types 72 | .get(*type_idx) 73 | .expect("type index of import descs to always be valid if the validation info is correct"); 74 | // TODO ugly clone that should disappear when types are directly parsed from bytecode instead of vector copies 75 | ExternType::Func(func_type.clone()) 76 | } 77 | ImportDesc::Table(ty) => ExternType::Table(*ty), 78 | ImportDesc::Mem(ty) => ExternType::Mem(*ty), 79 | ImportDesc::Global(ty) => ExternType::Global(*ty), 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/core/sidetable.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a data structure to allow in-place interpretation 2 | //! 3 | //! Control-flow in WASM is denoted in labels. To avoid linear search through the WASM binary or 4 | //! stack for the respective label of a branch, a sidetable is generated during validation, which 5 | //! stores the offset on the current instruction pointer for the branch. A sidetable entry hence 6 | //! allows to translate the implicit control flow information ("jump to the next `else`") to 7 | //! explicit modifications of the instruction pointer (`instruction_pointer += 13`). 8 | //! 9 | //! Branches in WASM can only go outwards, they either `break` out of a block or `continue` to the 10 | //! head of a loob block. Put differently, a label can only be referenced from within its 11 | //! associated structured control instruction. 12 | //! 13 | //! Noteworthy, branching can also have side-effects on the operand stack: 14 | //! 15 | //! - Taking a branch unwinds the operand stack, down to where the targeted structured control flow 16 | //! instruction was entered. [`SidetableEntry::popcnt`] holds information on how many values to 17 | //! pop from the operand stack when a branch is taken. 18 | //! - When a branch is taken, it may consume arguments from the operand stack. These are pushed 19 | //! back on the operand stack after unwinding. This behavior can be emulated by copying the 20 | //! uppermost [`SidetableEntry::valcnt`] operands on the operand stack before taking a branch 21 | //! into a structured control instruction. 22 | //! 23 | //! # Reference 24 | //! 25 | //! - Core / Syntax / Instructions / Control Instructions, WASM Spec, 26 | //! 27 | //! - "A fast in-place interpreter for WebAssembly", Ben L. Titzer, 28 | //! 29 | 30 | use alloc::vec::Vec; 31 | 32 | // A sidetable 33 | 34 | pub type Sidetable = Vec; 35 | 36 | /// Entry to translate the current branches implicit target into an explicit offset to the instruction pointer, as well as the side table pointer 37 | /// 38 | /// Each of the following constructs requires a [`SidetableEntry`]: 39 | /// 40 | /// - br 41 | /// - br_if 42 | /// - br_table 43 | /// - else 44 | // TODO hide implementation 45 | // TODO Remove Clone trait from sidetables 46 | #[derive(Debug, Clone)] 47 | pub struct SidetableEntry { 48 | /// Δpc: the amount to adjust the instruction pointer by if the branch is taken 49 | pub delta_pc: isize, 50 | 51 | /// Δstp: the amount to adjust the side-table index by if the branch is taken 52 | pub delta_stp: isize, 53 | 54 | /// valcnt: the number of values that will be copied if the branch is taken 55 | /// 56 | /// Branches may additionally consume operands themselves, which they push back on the operand 57 | /// stack after unwinding. 58 | pub valcnt: usize, 59 | 60 | /// popcnt: the number of values that will be popped if the branch is taken 61 | /// 62 | /// Taking a branch unwinds the operand stack down to the height where the targeted structured 63 | /// control instruction was entered. 64 | pub popcnt: usize, 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/pages_coverage_preview.yaml: -------------------------------------------------------------------------------- 1 | name: Coverage Preview Deploy 2 | 3 | on: 4 | # When a PR is merged (or force push to main) 5 | pull_request: 6 | types: 7 | - opened 8 | - reopened 9 | - synchronize 10 | - closed 11 | - labeled 12 | - unlabeled 13 | 14 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 15 | permissions: 16 | contents: write 17 | pull-requests: write 18 | 19 | concurrency: preview-cov-${{ github.ref }} 20 | 21 | jobs: 22 | deploy: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | submodules: true 28 | # -=-=-=-= Create report =-=-=-=- 29 | - uses: cachix/install-nix-action@v31 30 | if: | 31 | (github.event.action == 'labeled' && github.event.label.name == 'coverage') || 32 | (github.event.action != 'labeled' && github.event.action != 'unlabeled' && contains(github.event.pull_request.labels.*.name, 'coverage')) 33 | with: 34 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 35 | - uses: cachix/cachix-action@v16 36 | if: | 37 | (github.event.action == 'labeled' && github.event.label.name == 'coverage') || 38 | (github.event.action != 'labeled' && github.event.action != 'unlabeled' && contains(github.event.pull_request.labels.*.name, 'coverage')) 39 | with: 40 | name: dlr-ft 41 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 42 | - name: Generate report(s) 43 | if: | 44 | (github.event.action == 'labeled' && github.event.label.name == 'coverage') || 45 | (github.event.action != 'labeled' && github.event.action != 'unlabeled' && contains(github.event.pull_request.labels.*.name, 'coverage')) 46 | run: nix build .?submodules=1#report --print-build-logs 47 | # -=-=-=-= Deploy (when labeled) =-=-=-=- 48 | - name: Deploy Preview (labeled) 49 | if: ${{ github.event.action == 'labeled' && github.event.label.name == 'coverage' }} 50 | uses: rossjrw/pr-preview-action@v1.4.7 51 | with: 52 | source-dir: result/coverage/html/ 53 | umbrella-dir: coverage/pr-preview 54 | action: deploy # force deployment since, by default, this actions does nothing on the 'labeled' event 55 | # -=-=-=-= Deploy (when unlabeled) =-=-=-=- 56 | - name: Deploy Preview (unlabeled) 57 | if: ${{ github.event.action == 'unlabeled' && github.event.label.name == 'coverage' }} 58 | uses: rossjrw/pr-preview-action@v1.4.7 59 | with: 60 | source-dir: result/coverage/html/ 61 | umbrella-dir: coverage/pr-preview 62 | action: remove # force removal since, by default, this actions does nothing on the 'labeled' event 63 | # -=-=-=-= Deploy (default) =-=-=-=- 64 | - name: Deploy Preview (default) 65 | if: ${{ github.event.action != 'labeled' && github.event.action != 'unlabeled' && contains(github.event.pull_request.labels.*.name, 'coverage') }} 66 | uses: rossjrw/pr-preview-action@v1.4.7 67 | with: 68 | source-dir: result/coverage/html/ 69 | umbrella-dir: coverage/pr-preview 70 | -------------------------------------------------------------------------------- /examples/stuff/main.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::process::ExitCode; 3 | use std::str::FromStr; 4 | 5 | use log::{error, LevelFilter}; 6 | 7 | use wasm::{validate, Store}; 8 | 9 | fn main() -> ExitCode { 10 | let level = LevelFilter::from_str(&env::var("RUST_LOG").unwrap_or("TRACE".to_owned())).unwrap(); 11 | env_logger::builder().filter_level(level).init(); 12 | 13 | let wat = r#" 14 | (module 15 | (memory 1) 16 | (func $add_one (export "add_one") (param $x i32) (result i32) (local $ununsed_local i32) 17 | local.get $x 18 | i32.const 1 19 | i32.add) 20 | 21 | (func $add (export "add") (param $x i32) (param $y i32) (result i32) 22 | local.get $y 23 | local.get $x 24 | i32.add) 25 | 26 | (func (export "store_num") (param $x i32) 27 | i32.const 0 28 | local.get $x 29 | i32.store) 30 | (func (export "load_num") (result i32) 31 | i32.const 0 32 | i32.load) 33 | 34 | (export "add_one" (func $add_one)) 35 | (export "add" (func $add)) 36 | ) 37 | "#; 38 | let wasm_bytes = wat::parse_str(wat).unwrap(); 39 | 40 | let validation_info = match validate(&wasm_bytes) { 41 | Ok(table) => table, 42 | Err(err) => { 43 | error!("Validation failed: {err:?} [{err}]"); 44 | return ExitCode::FAILURE; 45 | } 46 | }; 47 | 48 | let mut store = Store::new(()); 49 | 50 | let module = match store.module_instantiate_unchecked(&validation_info, Vec::new(), None) { 51 | Ok(outcome) => outcome.module_addr, 52 | Err(err) => { 53 | error!("Instantiation failed: {err:?} [{err}]"); 54 | return ExitCode::FAILURE; 55 | } 56 | }; 57 | 58 | let add_one = store 59 | .instance_export_unchecked(module, "add_one") 60 | .unwrap() 61 | .as_func() 62 | .unwrap(); 63 | 64 | let add = store 65 | .instance_export_unchecked(module, "add") 66 | .unwrap() 67 | .as_func() 68 | .unwrap(); 69 | 70 | let store_num = store 71 | .instance_export_unchecked(module, "store_num") 72 | .unwrap() 73 | .as_func() 74 | .unwrap(); 75 | 76 | let load_num = store 77 | .instance_export_unchecked(module, "load_num") 78 | .unwrap() 79 | .as_func() 80 | .unwrap(); 81 | 82 | let twelve: i32 = store 83 | .invoke_typed_without_fuel_unchecked(add, (5, 7)) 84 | .unwrap(); 85 | assert_eq!(twelve, 12); 86 | 87 | let twelve_plus_one: i32 = store 88 | .invoke_typed_without_fuel_unchecked(add_one, twelve) 89 | .unwrap(); 90 | assert_eq!(twelve_plus_one, 13); 91 | 92 | store 93 | .invoke_typed_without_fuel_unchecked::<_, ()>(store_num, 42_i32) 94 | .unwrap(); 95 | 96 | assert_eq!( 97 | store 98 | .invoke_typed_without_fuel_unchecked::<(), i32>(load_num, ()) 99 | .unwrap(), 100 | 42_i32 101 | ); 102 | 103 | ExitCode::SUCCESS 104 | } 105 | -------------------------------------------------------------------------------- /src/core/reader/types/data.rs: -------------------------------------------------------------------------------- 1 | use core::fmt::{Debug, Formatter}; 2 | 3 | use alloc::{format, vec::Vec}; 4 | 5 | use crate::core::{indices::MemIdx, reader::span::Span}; 6 | 7 | #[derive(Clone)] 8 | pub struct DataSegment { 9 | pub init: Vec, 10 | pub mode: DataMode, 11 | } 12 | 13 | #[derive(Clone)] 14 | pub enum DataMode { 15 | Passive, 16 | Active(DataModeActive), 17 | } 18 | 19 | #[derive(Clone)] 20 | pub struct DataModeActive { 21 | pub memory_idx: MemIdx, 22 | pub offset: Span, 23 | } 24 | 25 | impl Debug for DataSegment { 26 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 27 | let mut init_str = alloc::string::String::new(); 28 | 29 | let iter = self.init.iter().peekable(); 30 | // only if it's valid do we print is as a normal utf-8 char, otherwise, hex 31 | for &byte in iter { 32 | if let Ok(valid_char) = alloc::string::String::from_utf8(Vec::from(&[byte])) { 33 | init_str.push_str(valid_char.as_str()); 34 | } else { 35 | init_str.push_str(&format!("\\x{byte:02x}")); 36 | } 37 | } 38 | 39 | f.debug_struct("DataSegment") 40 | .field("init", &init_str) 41 | .field("mode", &self.mode) 42 | .finish() 43 | } 44 | } 45 | 46 | /// 47 | /// Usually, we'd have something like this: 48 | /// ```wasm 49 | /// (module 50 | /// (memory 1) ;; memory starting with 1 page 51 | /// (data (i32.const 0) "abc") ;; writing the array of byte "abc" in the first memory (0) at offset 0 52 | /// ;; for hardcoded offsets, we'll usually use i32.const because of wasm being x86 arch 53 | /// ) 54 | /// ``` 55 | /// 56 | /// Since the span has only the start and length and acts a reference, we print the start and end (both inclusive, notice the '..=') 57 | /// We print it in both decimal and hexadecimal so it's easy to trace in something like 58 | impl Debug for DataMode { 59 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 60 | match self { 61 | DataMode::Passive => f.debug_struct("Passive").finish(), 62 | DataMode::Active(active_data_mode) => { 63 | let from = active_data_mode.offset.from; 64 | let to = active_data_mode.offset.from + active_data_mode.offset.len() - 1; 65 | f.debug_struct("Active") 66 | // .field("offset", format_args!("[{}..={}]", from, to)) 67 | .field( 68 | "offset", 69 | &format_args!("[{from}..={to}] (hex = [{from:X}..={to:X}])"), 70 | ) 71 | .finish() 72 | // f. 73 | } 74 | } 75 | } 76 | } 77 | 78 | pub struct _PassiveData { 79 | pub init: Vec, 80 | } 81 | 82 | impl Debug for _PassiveData { 83 | fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { 84 | let mut init_str = alloc::string::String::new(); 85 | 86 | let iter = self.init.iter().peekable(); 87 | for &byte in iter { 88 | if let Ok(valid_char) = alloc::string::String::from_utf8(Vec::from(&[byte])) { 89 | init_str.push_str(valid_char.as_str()); 90 | } else { 91 | // If it's not valid UTF-8, print it as hex 92 | init_str.push_str(&format!("\\x{byte:02x}")); 93 | } 94 | } 95 | f.debug_struct("PassiveData") 96 | .field("init", &init_str) 97 | .finish() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/execution/resumable.rs: -------------------------------------------------------------------------------- 1 | use core::num::NonZeroU32; 2 | 3 | use alloc::{ 4 | sync::{Arc, Weak}, 5 | vec::Vec, 6 | }; 7 | 8 | use crate::{ 9 | addrs::FuncAddr, 10 | core::slotmap::{SlotMap, SlotMapKey}, 11 | rw_spinlock::RwSpinLock, 12 | value_stack::Stack, 13 | Value, 14 | }; 15 | 16 | #[derive(Debug)] 17 | pub(crate) struct Resumable { 18 | pub(crate) stack: Stack, 19 | pub(crate) pc: usize, 20 | pub(crate) stp: usize, 21 | pub(crate) current_func_addr: FuncAddr, 22 | pub(crate) maybe_fuel: Option, 23 | } 24 | 25 | #[derive(Default)] 26 | pub(crate) struct Dormitory(pub(crate) Arc>>); 27 | 28 | impl Dormitory { 29 | #[allow(unused)] 30 | pub(crate) fn new() -> Self { 31 | Self::default() 32 | } 33 | 34 | pub(crate) fn insert(&self, resumable: Resumable) -> InvokedResumableRef { 35 | let key = self.0.write().insert(resumable); 36 | 37 | InvokedResumableRef { 38 | dormitory: Arc::downgrade(&self.0), 39 | key, 40 | } 41 | } 42 | } 43 | 44 | pub struct InvokedResumableRef { 45 | pub(crate) dormitory: Weak>>, 46 | pub(crate) key: SlotMapKey, 47 | } 48 | 49 | pub struct FreshResumableRef { 50 | pub(crate) func_addr: FuncAddr, 51 | pub(crate) params: Vec, 52 | pub(crate) maybe_fuel: Option, 53 | } 54 | 55 | /// An object associated to a resumable that is held internally. 56 | pub enum ResumableRef { 57 | /// indicates this resumable has never been invoked/resumed to. 58 | Fresh(FreshResumableRef), 59 | /// indicates this resumable has been invoked/resumed to at least once. 60 | Invoked(InvokedResumableRef), 61 | } 62 | 63 | impl Drop for InvokedResumableRef { 64 | fn drop(&mut self) { 65 | let Some(dormitory) = self.dormitory.upgrade() else { 66 | // Either the dormitory was already dropped or `self` was used to finish execution. 67 | return; 68 | }; 69 | 70 | dormitory.write().remove(&self.key) 71 | .expect("that the resumable could not have been removed already, because then this self could not exist or the dormitory weak pointer would have been None"); 72 | } 73 | } 74 | 75 | /// Represents the state of a possibly interrupted resumable. 76 | pub enum RunState { 77 | /// represents a resumable that has executed completely with return values `values` and possibly remaining fuel 78 | /// `maybe_remaining_fuel` (has `Some(remaining_fuel)` for fuel-metered operations and `None` otherwise) 79 | Finished { 80 | values: Vec, 81 | maybe_remaining_fuel: Option, 82 | }, 83 | /// represents a resumable that has ran out of fuel during execution, missing at least `required_fuel` units of fuel 84 | /// to continue further execution. 85 | Resumable { 86 | resumable_ref: ResumableRef, 87 | required_fuel: NonZeroU32, 88 | }, 89 | } 90 | 91 | #[cfg(test)] 92 | mod test { 93 | use crate::{addrs::FuncAddr, value_stack::Stack}; 94 | 95 | use super::{Dormitory, Resumable}; 96 | 97 | /// Test that a dormitory can be constructed and that a resumable can be inserted 98 | #[test] 99 | fn dormitory_constructor() { 100 | let dorm = Dormitory::new(); 101 | 102 | let resumable = Resumable { 103 | stack: Stack::new(), 104 | pc: 11, 105 | stp: 13, 106 | current_func_addr: FuncAddr::INVALID, 107 | maybe_fuel: None, 108 | }; 109 | 110 | dorm.insert(resumable); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wasm-interpreter 2 | 3 |

4 | Website  •  5 | Features  •  6 | Resources 7 |

8 |

9 | ci status 10 | code coverage 11 | license 12 | license 13 |

14 | 15 | A minimal in-place interpreter for [WebAssembly](https://webassembly.org/) bytecode (almost without) dependencies while being `no_std`. 16 | 17 | ## Features 18 | 19 | - **In-place interpretation**: No intermediate representation, directly interprets WebAssembly bytecode. This allows for fast start-up times. 20 | - **`no_std` support**: The interpreter requires only Rust's `core` and `alloc` libraries allowing its use in various environments, such as bare-metal systems. 21 | - **Minimal dependencies**: The interpreter requires only two dependencies: `log`, `libm`. 22 | - **Compliance with specification**: The interpreter passes all tests from the [official WebAssembly testsuite](https://github.com/WebAssembly/testsuite), except for the unfinished proposal tests. See [`GlobalConfig` in `tests/specification/mod.rs`](tests/specification/mod.rs) for the default spec-test filter regex. 23 | - **Host functions**: The host system can provide functions for Wasm code to call. 24 | - **Fuel & resumable execution**: A fuel mechanism is used to halt execution once fuel runs out. Then fuel can be refilled and execution resumed. 25 | 26 | _For information on other features, visit our [requirements page](https://dlr-ft.github.io/wasm-interpreter/main/requirements/html/index.html)._ 27 | 28 | ### Planned 29 | 30 | - **C bindings**: The interpreter can be used from C code. 31 | - **Migratability**: Wasm instances can be transferred between systems during their execution. 32 | - **Threading**: There are multiple threading proposals, but we have not yet chosen a specific one. Some options are [shared-everything-threads](https://github.com/WebAssembly/shared-everything-threads), [threads](https://github.com/WebAssembly/threads), [wasi-threads](https://github.com/WebAssembly/wasi-threads). 33 | 34 | ### Not planned 35 | 36 | Multi-memory proposal, GC proposal 37 | 38 | ## Resources 39 | 40 | - `A fast in-place interpreter` by Ben L. Titzer: https://arxiv.org/abs/2205.01183 41 | - WebAssembly spec: https://webassembly.github.io/spec/core/index.html 42 | - WebAssembly Opcode Table: https://pengowray.github.io/wasm-ops/ 43 | - Compiler/Interpreter Know-How Gist Compilation: https://gist.github.com/o11c/6b08643335388bbab0228db763f99219 44 | - Mozilla Developer Network WebAssembly Homepage: https://developer.mozilla.org/en-US/docs/WebAssembly 45 | 46 | ## License 47 | 48 | Licensed under either of 49 | 50 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 51 | - MIT License ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 52 | 53 | at your option. 54 | 55 | ## Contribution 56 | 57 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 58 | 59 | ## Copyright 60 | 61 | Copyright © 2024-2025 Deutsches Zentrum für Luft- und Raumfahrt e.V. (DLR) 62 | Copyright © 2024-2025 OxidOS Automotive SRL 63 | -------------------------------------------------------------------------------- /crates/compare-testsuite-rs/src/summary.rs: -------------------------------------------------------------------------------- 1 | use std::{borrow::Cow, fmt::Display}; 2 | 3 | use crate::{ 4 | ci_reports::{CIFullReport, CIReportData, CIReportHeader}, 5 | sanitize_path, write_details_summary, 6 | }; 7 | 8 | pub fn generate(report: &CIFullReport) -> ReportSummary { 9 | // No logic here 10 | // The analysis done for the summary is minimal and can be done in the Display trait to avoid code duplication 11 | ReportSummary(report) 12 | } 13 | 14 | pub struct ReportSummary<'report>(&'report CIFullReport); 15 | 16 | impl Display for ReportSummary<'_> { 17 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 18 | writeln!(f, "## Per-test details")?; 19 | writeln!(f)?; 20 | write_details_summary( 21 | f, 22 | |f| write!(f, "Click here to open"), 23 | |f| { 24 | write!( 25 | f, 26 | " 27 | 28 | | **File** | **Passed Asserts** | **Failed Asserts** | **% Passed** | **Notes** | 29 | |:--------:|:------------------:|:------------------:|:------------:|-----------| 30 | " 31 | )?; 32 | self.0 33 | .entries 34 | .iter() 35 | .try_for_each(|CIReportHeader { filepath, data }| { 36 | write!(f, 37 | "| [{filename}](https://github.com/WebAssembly/testsuite/blob/main/{filename}) | ", 38 | filename = sanitize_path(filepath) 39 | )?; 40 | 41 | match data { 42 | CIReportData::Assert { results } => { 43 | let num_total = results.len(); 44 | let num_success = results 45 | .iter() 46 | .filter(|assert| assert.error.is_none()) 47 | .count(); 48 | let num_failed = num_total - num_success; 49 | let success_percentage = 50 | (num_total != 0).then(|| 100.0 * num_success as f32 / num_total as f32); 51 | 52 | write!( 53 | f, 54 | "{num_success} / {num_total} | {num_failed} / {num_total} | " 55 | )?; 56 | if let Some(success_percentage) = success_percentage { 57 | write!(f, "{success_percentage}% | ")?; 58 | } else { 59 | write!(f, "- |")?; 60 | } 61 | 62 | write!(f, "- |")?; 63 | } 64 | CIReportData::ScriptError { 65 | context, 66 | line_number, 67 | .. 68 | } => { 69 | write!(f, "- | - | - | ")?; 70 | write!(f, "Context: {}
", sanitize_table_item(Some(context)))?; // # TODO when is this supposed to be None? 71 | if let Some(line_number) = line_number { 72 | write!(f, "Line: {line_number} |")?; 73 | } else { 74 | write!(f, "Line: - |")?; 75 | } 76 | } 77 | } 78 | writeln!(f) 79 | }) 80 | }, 81 | ) 82 | } 83 | } 84 | 85 | fn sanitize_table_item(item: Option<&str>) -> Cow<'static, str> { 86 | let Some(item) = item else { 87 | return Cow::Borrowed("-"); 88 | }; 89 | 90 | let item = item 91 | .chars() 92 | .map(|c| match c { 93 | '`' => '\'', 94 | '|' => '/', 95 | '\n' => ' ', 96 | x => x, 97 | }) 98 | .collect(); 99 | 100 | Cow::Owned(item) 101 | } 102 | -------------------------------------------------------------------------------- /pkgs/whitepaper/refs.yaml: -------------------------------------------------------------------------------- 1 | # Heper to convert from BibTeX: 2 | # https://jonasloos.github.io/bibtex-to-hayagriva-webapp/ 3 | 4 | # papers 5 | 6 | webassembly-in-avionics-paper: 7 | type: article 8 | title: "WebAssembly in Avionics : Decoupling Software from Hardware" 9 | author: 10 | - Zaeske, Wanja 11 | - Friedrich, Sven 12 | - Schubert, Tim 13 | - Durak, Umut 14 | date: 2023 15 | page-range: 1–10 16 | serial-number: 17 | doi: 10.1109/DASC58513.2023.10311207 18 | parent: 19 | type: proceedings 20 | title: 2023 IEEE/AIAA 42nd Digital Avionics Systems Conference (DASC) 21 | issue: "" 22 | volume: 0 23 | 24 | fast-in-place-interpreter-for-webassembly: 25 | type: article 26 | title: A fast in-place interpreter for WebAssembly 27 | author: Titzer, Ben L. 28 | date: 2022-10 29 | url: https://doi.org/10.1145/3563311 30 | serial-number: 31 | doi: 10.1145/3563311 32 | parent: 33 | type: periodical 34 | title: Proc. ACM Program. Lang. 35 | publisher: Association for Computing Machinery 36 | location: New York, NY, USA 37 | issue: OOPSLA2 38 | volume: 6 39 | 40 | # standards etc. 41 | 42 | do178c: 43 | type: report 44 | title: "{DO-178C} Software Considerations in Airborne Systems and Equipment Certification" 45 | author: RTCA 46 | date: 2011 47 | organization: RTCA 48 | do330: 49 | type: report 50 | title: "{DO-330} Software Tool Qualification Considerations" 51 | author: RTCA 52 | date: 2011 53 | organization: RTCA 54 | 55 | do331: 56 | type: report 57 | title: "{DO-331} Model-Based Development and Verification Supplement to {DO-178C} and {DO-278A}" 58 | author: RTCA 59 | date: 2011 60 | organization: RTCA 61 | 62 | do332: 63 | type: report 64 | title: "{DO-332} Object-Oriented Technology and Related Techniques Supplement to {DO-178C} and {DO-278A}" 65 | author: RTCA 66 | date: 2011 67 | organization: RTCA 68 | 69 | do333: 70 | type: report 71 | title: "{DO-333} Formal Methods Supplement to {DO-178C} and {DO-278A}" 72 | author: RTCA 73 | date: 2011 74 | organization: RTCA 75 | 76 | # websites 77 | 78 | our-repo: 79 | type: Web 80 | title: wasm-interpreter 81 | author: 82 | - DLR e. V. 83 | - OxidOS 84 | url: https://github.com/DLR-FT/wasm-interpreter 85 | 86 | dlr-ft-website: 87 | type: Web 88 | title: DLR | Institute of Flight Systems 89 | author: DLR e. V. 90 | url: https://www.dlr.de/en/ft 91 | 92 | oxidos-website: 93 | type: Web 94 | title: Welcome to OxidOS Automotive 95 | author: OxidOS 96 | url: https://oxidos.io/ 97 | 98 | wasmtime-website: 99 | type: Web 100 | title: A fast and secuire runtime for Webassembly 101 | author: Bytecode Alliance 102 | url: https://wasmtime.dev/ 103 | 104 | mozilla-baseline-compiler: 105 | type: Web 106 | title: "Making WebAssembly even faster: Firefox’s new streaming and tiering compiler" 107 | author: Lin Clark 108 | url: https://hacks.mozilla.org/2018/01/making-webassembly-even-faster-firefoxs-new-streaming-and-tiering-compiler/ 109 | 110 | wasmi-instruction-enum: 111 | type: Web 112 | title: "Making WebAssembly even faster: Firefox’s new streaming and tiering compiler" 113 | author: Robin Freyler et al. 114 | url: https://docs.rs/wasmi_ir/latest/wasmi_ir/enum.Instruction.html 115 | 116 | wizard-engine: 117 | type: Web 118 | title: The Wizard Research Engine 119 | author: Ben L. Titzer et al. 120 | url: https://github.com/titzer/wizard-engine 121 | 122 | volvo-rust-assembly-line: 123 | type: Web 124 | title: Rust is rolling off the Volvo assembly line 125 | author: 126 | - Dion Dokte 127 | - Julius Gustavsson 128 | url: https://tweedegolf.nl/en/blog/137/rust-is-rolling-off-the-volvo-assembly-line 129 | 130 | ferrous-systems-website: 131 | type: Web 132 | title: Ferrous Systems 133 | url: https://ferrous-systems.com/ 134 | 135 | ferrocene-website: 136 | type: Web 137 | title: This is Rust for critical systems. 138 | url: https://ferrocene.dev/ 139 | 140 | adacore-website: 141 | type: Web 142 | title: AdaCore 143 | url: https://www.adacore.com/ 144 | 145 | adacore-rust-website: 146 | type: Web 147 | title: GNAT Pro for Rust 148 | url: https://www.adacore.com/gnatpro-rust 149 | -------------------------------------------------------------------------------- /.github/workflows/nix.yaml: -------------------------------------------------------------------------------- 1 | name: Nix based CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "gh-readonly-queue/**" 7 | - "gh-pages" 8 | merge_group: 9 | 10 | jobs: 11 | checks: 12 | name: Nix flake check 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | submodules: true 18 | - uses: cachix/install-nix-action@v31 19 | with: 20 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 21 | - uses: cachix/cachix-action@v16 22 | with: 23 | name: dlr-ft 24 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 25 | - run: nix flake check .?submodules=1 26 | 27 | bench: 28 | name: Nix benchmark 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v4 32 | with: 33 | submodules: true 34 | - uses: cachix/install-nix-action@v31 35 | with: 36 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 37 | - uses: cachix/cachix-action@v16 38 | with: 39 | name: dlr-ft 40 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 41 | - run: nix build .?submodules=1#benchmark --print-build-logs 42 | 43 | build: 44 | name: Nix build and test 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | with: 49 | submodules: true 50 | - uses: cachix/install-nix-action@v31 51 | with: 52 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 53 | - uses: cachix/cachix-action@v16 54 | with: 55 | name: dlr-ft 56 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 57 | - run: nix build .?submodules=1#wasm-interpreter --print-build-logs 58 | 59 | cover: 60 | name: Nix coverage 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v4 64 | with: 65 | submodules: true 66 | - uses: cachix/install-nix-action@v31 67 | with: 68 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 69 | - uses: cachix/cachix-action@v16 70 | with: 71 | name: dlr-ft 72 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 73 | - run: nix build .?submodules=1#coverage --print-build-logs 74 | - name: Upload coverage to Codecov 75 | uses: codecov/codecov-action@v4 76 | with: 77 | verbose: true 78 | file: result/lcov-codecov.json 79 | env: 80 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 81 | 82 | report: 83 | name: Nix report 84 | needs: [bench, build, cover, requirements] 85 | runs-on: ubuntu-latest 86 | steps: 87 | - uses: actions/checkout@v4 88 | with: 89 | submodules: true 90 | - uses: cachix/install-nix-action@v31 91 | with: 92 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 93 | - uses: cachix/cachix-action@v16 94 | with: 95 | name: dlr-ft 96 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 97 | - run: nix build .?submodules=1#report --print-build-logs 98 | - name: Archive report 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: report 102 | path: result/ 103 | 104 | requirements: 105 | name: Nix generate requirements 106 | runs-on: ubuntu-latest 107 | steps: 108 | - uses: actions/checkout@v4 109 | with: 110 | submodules: true 111 | - uses: cachix/install-nix-action@v31 112 | with: 113 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 114 | - uses: cachix/cachix-action@v16 115 | with: 116 | name: dlr-ft 117 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 118 | - run: nix build .?submodules=1#requirements --print-build-logs 119 | 120 | build-test-32bit: 121 | name: Test for 32 bit target 122 | runs-on: ubuntu-latest 123 | steps: 124 | - uses: actions/checkout@v4 125 | with: 126 | submodules: true 127 | - uses: cachix/install-nix-action@v31 128 | with: 129 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 130 | - uses: cachix/cachix-action@v16 131 | with: 132 | name: dlr-ft 133 | authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} 134 | - run: nix build .?submodules=1#packages.i686-linux.wasm-interpreter --print-build-logs 135 | -------------------------------------------------------------------------------- /tests/function_recursion.rs: -------------------------------------------------------------------------------- 1 | use wasm::{validate, Store}; 2 | 3 | const FUNCTION_CALL: &str = r#" 4 | (module 5 | (func (export "simple_caller") (param $x i32) (param $y i32) (result i32) 6 | (call $callee (i32.mul (local.get $x) (local.get $y))) 7 | ) 8 | (func $callee (param $x i32) (result i32) 9 | local.get $x 10 | i32.const 13 11 | i32.add 12 | ) 13 | ) 14 | "#; 15 | 16 | /// A simple function to multiply two numbers, then calling another function to add 13 17 | #[test_log::test] 18 | fn simple_function_call() { 19 | let wasm_bytes = wat::parse_str(FUNCTION_CALL).unwrap(); 20 | 21 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 22 | let mut store = Store::new(()); 23 | let module = store 24 | .module_instantiate(&validation_info, Vec::new(), None) 25 | .unwrap() 26 | .module_addr; 27 | 28 | let simple_caller = store 29 | .instance_export(module, "simple_caller") 30 | .unwrap() 31 | .as_func() 32 | .unwrap(); 33 | 34 | assert_eq!( 35 | 3 * 7 + 13, 36 | store 37 | .invoke_typed_without_fuel(simple_caller, (3, 7)) 38 | .unwrap() 39 | ); 40 | } 41 | 42 | /// A simple function to add 2 to an i32 using a recusive call to "add_one" and return the result 43 | #[test_log::test] 44 | fn recursion_valid() { 45 | use wasm::{validate, Store}; 46 | 47 | let wat = r#" 48 | (module 49 | (func $add_one (export "add_one") (param $x i32) (result i32) 50 | local.get $x 51 | i32.const 1 52 | i32.add 53 | ) 54 | (func (export "add_two") (param $x i32) (result i32) 55 | local.get $x 56 | call $add_one 57 | call $add_one 58 | ) 59 | ) 60 | "#; 61 | let wasm_bytes = wat::parse_str(wat).unwrap(); 62 | 63 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 64 | let mut store = Store::new(()); 65 | let module = store 66 | .module_instantiate(&validation_info, Vec::new(), None) 67 | .unwrap() 68 | .module_addr; 69 | 70 | let add_two = store 71 | .instance_export(module, "add_two") 72 | .unwrap() 73 | .as_func() 74 | .unwrap(); 75 | 76 | assert_eq!(12, store.invoke_typed_without_fuel(add_two, 10).unwrap()); 77 | assert_eq!(2, store.invoke_typed_without_fuel(add_two, 0).unwrap()); 78 | assert_eq!(-4, store.invoke_typed_without_fuel(add_two, -6).unwrap()); 79 | } 80 | 81 | #[test_log::test] 82 | fn recursion_busted_stack() { 83 | use wasm::{validate, ValidationError}; 84 | 85 | let wat = r#" 86 | (module 87 | (func $add_one (export "add_one") (param $x i32) (result i32 i32) 88 | local.get $x 89 | i32.const 1 90 | i32.add 91 | local.get $x 92 | i32.const 1 93 | i32.add 94 | ) 95 | (func (export "add_two") (param $x i32) (result i32) 96 | local.get $x 97 | call $add_one 98 | call $add_one 99 | ) 100 | ) 101 | "#; 102 | let wasm_bytes = wat::parse_str(wat).unwrap(); 103 | 104 | assert_eq!( 105 | validate(&wasm_bytes).err(), 106 | Some(ValidationError::EndInvalidValueStack), 107 | "validation incorrectly passed" 108 | ); 109 | } 110 | 111 | #[test_log::test] 112 | fn multivalue_call() { 113 | let wat = r#" 114 | (module 115 | (func $foo (param $x i64) (param $y i32) (param $z f32) (result i32 f32 i64) 116 | local.get $y 117 | local.get $z 118 | local.get $x 119 | ) 120 | (func $bar (export "bar") (result i32 f32 i64) 121 | i64.const 5 122 | i32.const 10 123 | f32.const 42.0 124 | call $foo 125 | ) 126 | ) 127 | "#; 128 | let wasm_bytes = wat::parse_str(wat).unwrap(); 129 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 130 | let mut store = Store::new(()); 131 | let module = store 132 | .module_instantiate(&validation_info, Vec::new(), None) 133 | .unwrap() 134 | .module_addr; 135 | 136 | let foo = store 137 | .instance_export(module, "bar") 138 | .unwrap() 139 | .as_func() 140 | .unwrap(); 141 | 142 | assert_eq!( 143 | (10, 42.0, 5), 144 | store 145 | .invoke_typed_without_fuel::<(), (i32, f32, i64)>(foo, ()) 146 | .unwrap() 147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /tests/memory_redundancy.rs: -------------------------------------------------------------------------------- 1 | /* 2 | # This file incorporates code from the WebAssembly testsuite, originally 3 | # available at https://github.com/WebAssembly/testsuite. 4 | # 5 | # The original code is licensed under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | */ 17 | use hexf::hexf32; 18 | use wasm::{validate, Store}; 19 | 20 | #[test_log::test] 21 | fn memory_redundancy() { 22 | let w = r#" 23 | (module 24 | (memory 1 1) 25 | 26 | (func (export "zero_everything") 27 | (i32.store (i32.const 0) (i32.const 0)) 28 | (i32.store (i32.const 4) (i32.const 0)) 29 | (i32.store (i32.const 8) (i32.const 0)) 30 | (i32.store (i32.const 12) (i32.const 0)) 31 | ) 32 | 33 | (func (export "test_store_to_load") (result i32) 34 | (i32.store (i32.const 8) (i32.const 0)) 35 | (f32.store (i32.const 5) (f32.const -0.0)) 36 | (i32.load (i32.const 8)) 37 | ) 38 | 39 | (func (export "test_redundant_load") (result i32) 40 | (local $t i32) 41 | (local $s i32) 42 | (local.set $t (i32.load (i32.const 8))) 43 | (i32.store (i32.const 5) (i32.const 0x80000000)) 44 | (local.set $s (i32.load (i32.const 8))) 45 | (i32.add (local.get $t) (local.get $s)) 46 | ) 47 | 48 | (func (export "test_dead_store") (result f32) 49 | (local $t f32) 50 | (i32.store (i32.const 8) (i32.const 0x23232323)) 51 | (local.set $t (f32.load (i32.const 11))) 52 | (i32.store (i32.const 8) (i32.const 0)) 53 | (local.get $t) 54 | ) 55 | 56 | ;; A function named "malloc" which implementations nonetheless shouldn't 57 | ;; assume behaves like C malloc. 58 | (func $malloc (export "malloc") 59 | (param $size i32) 60 | (result i32) 61 | (i32.const 16) 62 | ) 63 | 64 | ;; Call malloc twice, but unlike C malloc, we don't get non-aliasing pointers. 65 | (func (export "malloc_aliasing") 66 | (result i32) 67 | (local $x i32) 68 | (local $y i32) 69 | (local.set $x (call $malloc (i32.const 4))) 70 | (local.set $y (call $malloc (i32.const 4))) 71 | (i32.store (local.get $x) (i32.const 42)) 72 | (i32.store (local.get $y) (i32.const 43)) 73 | (i32.load (local.get $x)) 74 | ) 75 | ) 76 | "#; 77 | let wasm_bytes = wat::parse_str(w).unwrap(); 78 | let validation_info = validate(&wasm_bytes).unwrap(); 79 | let mut store = Store::new(()); 80 | let module = store 81 | .module_instantiate(&validation_info, Vec::new(), None) 82 | .unwrap() 83 | .module_addr; 84 | 85 | let test_store_to_load = store 86 | .instance_export(module, "test_store_to_load") 87 | .unwrap() 88 | .as_func() 89 | .unwrap(); 90 | let zero_everything = store 91 | .instance_export(module, "zero_everything") 92 | .unwrap() 93 | .as_func() 94 | .unwrap(); 95 | let test_redundant_load = store 96 | .instance_export(module, "test_redundant_load") 97 | .unwrap() 98 | .as_func() 99 | .unwrap(); 100 | let test_dead_store = store 101 | .instance_export(module, "test_dead_store") 102 | .unwrap() 103 | .as_func() 104 | .unwrap(); 105 | let malloc_aliasing = store 106 | .instance_export(module, "malloc_aliasing") 107 | .unwrap() 108 | .as_func() 109 | .unwrap(); 110 | 111 | assert_eq!( 112 | store.invoke_typed_without_fuel(test_store_to_load, ()), 113 | Ok(0x00000080) 114 | ); 115 | store 116 | .invoke_typed_without_fuel::<(), ()>(zero_everything, ()) 117 | .unwrap(); 118 | assert_eq!( 119 | store.invoke_typed_without_fuel(test_redundant_load, ()), 120 | Ok(0x00000080) 121 | ); 122 | store 123 | .invoke_typed_without_fuel::<(), ()>(zero_everything, ()) 124 | .unwrap(); 125 | assert_eq!( 126 | store.invoke_typed_without_fuel(test_dead_store, ()), 127 | Ok(hexf32!("0x1.18p-144")) 128 | ); 129 | store 130 | .invoke_typed_without_fuel::<(), ()>(zero_everything, ()) 131 | .unwrap(); 132 | assert_eq!(store.invoke_typed_without_fuel(malloc_aliasing, ()), Ok(43)); 133 | } 134 | -------------------------------------------------------------------------------- /tests/memory_trap.rs: -------------------------------------------------------------------------------- 1 | /* 2 | # This file incorporates code from the WebAssembly testsuite, originally 3 | # available at https://github.com/WebAssembly/testsuite. 4 | # 5 | # The original code is licensed under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | */ 17 | use wasm::{validate, RuntimeError, Store, TrapError}; 18 | 19 | #[test_log::test] 20 | fn memory_trap_1() { 21 | let w = r#" 22 | (module 23 | (memory 1) 24 | 25 | (func $addr_limit (result i32) 26 | (i32.mul (memory.size) (i32.const 0x10000)) 27 | ) 28 | 29 | (func (export "store") (param $i i32) (param $v i32) 30 | (i32.store (i32.add (call $addr_limit) (local.get $i)) (local.get $v)) 31 | ) 32 | 33 | (func (export "load") (param $i i32) (result i32) 34 | (i32.load (i32.add (call $addr_limit) (local.get $i))) 35 | ) 36 | 37 | (func (export "memory.grow") (param i32) (result i32) 38 | (memory.grow (local.get 0)) 39 | ) 40 | ) 41 | "#; 42 | let wasm_bytes = wat::parse_str(w).unwrap(); 43 | let validation_info = validate(&wasm_bytes).unwrap(); 44 | let mut store = Store::new(()); 45 | let module = store 46 | .module_instantiate(&validation_info, Vec::new(), None) 47 | .unwrap() 48 | .module_addr; 49 | 50 | let store_func = store 51 | .instance_export(module, "store") 52 | .unwrap() 53 | .as_func() 54 | .unwrap(); 55 | let load = store 56 | .instance_export(module, "load") 57 | .unwrap() 58 | .as_func() 59 | .unwrap(); 60 | let grow = store 61 | .instance_export(module, "memory.grow") 62 | .unwrap() 63 | .as_func() 64 | .unwrap(); 65 | 66 | assert_eq!( 67 | store.invoke_typed_without_fuel(store_func, (-4, 42)), 68 | Ok(()) 69 | ); 70 | assert_eq!(store.invoke_typed_without_fuel(load, -4), Ok(42)); 71 | assert_eq!( 72 | store 73 | .invoke_typed_without_fuel::<(i32, i32), ()>(store_func, (-3, 0x12345678)) 74 | .err(), 75 | Some(RuntimeError::Trap(TrapError::MemoryOrDataAccessOutOfBounds)) 76 | ); 77 | assert_eq!( 78 | store.invoke_typed_without_fuel::(load, -3).err(), 79 | Some(RuntimeError::Trap(TrapError::MemoryOrDataAccessOutOfBounds)) 80 | ); 81 | assert_eq!( 82 | store 83 | .invoke_typed_without_fuel::<(i32, i32), ()>(store_func, (-2, 13)) 84 | .err(), 85 | Some(RuntimeError::Trap(TrapError::MemoryOrDataAccessOutOfBounds)) 86 | ); 87 | assert_eq!( 88 | store.invoke_typed_without_fuel::(load, -2).err(), 89 | Some(RuntimeError::Trap(TrapError::MemoryOrDataAccessOutOfBounds)) 90 | ); 91 | assert_eq!( 92 | store 93 | .invoke_typed_without_fuel::<(i32, i32), ()>(store_func, (-1, 13)) 94 | .err(), 95 | Some(RuntimeError::Trap(TrapError::MemoryOrDataAccessOutOfBounds)) 96 | ); 97 | assert_eq!( 98 | store.invoke_typed_without_fuel::(load, -1).err(), 99 | Some(RuntimeError::Trap(TrapError::MemoryOrDataAccessOutOfBounds)) 100 | ); 101 | assert_eq!( 102 | store 103 | .invoke_typed_without_fuel::<(i32, i32), ()>(store_func, (0, 13)) 104 | .err(), 105 | Some(RuntimeError::Trap(TrapError::MemoryOrDataAccessOutOfBounds)) 106 | ); 107 | assert_eq!( 108 | store.invoke_typed_without_fuel::(load, 0).err(), 109 | Some(RuntimeError::Trap(TrapError::MemoryOrDataAccessOutOfBounds)) 110 | ); 111 | assert_eq!( 112 | store 113 | .invoke_typed_without_fuel::<(i32, i32), ()>(store_func, (0x80000000_u32 as i32, 13)) 114 | .err(), 115 | Some(RuntimeError::Trap(TrapError::MemoryOrDataAccessOutOfBounds)) 116 | ); 117 | assert_eq!( 118 | store 119 | .invoke_typed_without_fuel::(load, 0x80000000_u32 as i32) 120 | .err(), 121 | Some(RuntimeError::Trap(TrapError::MemoryOrDataAccessOutOfBounds)) 122 | ); 123 | assert_eq!(store.invoke_typed_without_fuel(grow, 0x10001), Ok(-1)); 124 | } 125 | -------------------------------------------------------------------------------- /tests/linker.rs: -------------------------------------------------------------------------------- 1 | use wasm::{checked::StoredValue, linker::Linker, validate, RuntimeError, Store}; 2 | 3 | const SIMPLE_IMPORT_BASE: &str = r#" 4 | (module 5 | (import "addon" "get_one" (func $get_one (param) (result i32))) 6 | (func (export "get_three") (param) (result i32) 7 | call $get_one 8 | i32.const 2 9 | i32.add 10 | ) 11 | )"#; 12 | 13 | const SIMPLE_IMPORT_ADDON: &str = r#" 14 | (module 15 | (func (export "get_one") (param) (result i32) 16 | i32.const 1 17 | ) 18 | )"#; 19 | 20 | #[test_log::test] 21 | pub fn compile_simple_import() { 22 | let wasm_bytes_addon = wat::parse_str(SIMPLE_IMPORT_ADDON).unwrap(); 23 | let validation_info_addon = validate(&wasm_bytes_addon).expect("validation failed"); 24 | 25 | let wasm_bytes_base = wat::parse_str(SIMPLE_IMPORT_BASE).unwrap(); 26 | let validation_info_base = validate(&wasm_bytes_base).expect("validation failed"); 27 | 28 | let mut store = Store::new(()); 29 | let mut linker = Linker::new(); 30 | 31 | // First instantiate the addon module 32 | let addon = linker 33 | .module_instantiate(&mut store, &validation_info_addon, None) 34 | .unwrap() 35 | .module_addr; 36 | // We also want to define all of its exports, to makes them discoverable for 37 | // linking of the base module. 38 | linker 39 | .define_module_instance(&store, "addon".to_owned(), addon) 40 | .unwrap(); 41 | 42 | // Now we link and instantiate the base module. We can also perform linking 43 | // and instantiating them separately instead of going through 44 | // `Linker::module_instantiate`. This lets us inspect the linked extern 45 | // values in between. 46 | 47 | // 1. Perform linking 48 | let linked_base_imports = linker.instantiate_pre(&validation_info_base).unwrap(); 49 | 50 | // 1.5 Freely inspect the linked extern values 51 | assert_eq!( 52 | &*linked_base_imports, 53 | &[store.instance_export(addon, "get_one").unwrap()] 54 | ); 55 | 56 | // 2. Perform the actual instantiation directly on the `Store` 57 | let base = store 58 | .module_instantiate(&validation_info_base, linked_base_imports, None) 59 | .unwrap() 60 | .module_addr; 61 | 62 | let get_three = store 63 | .instance_export(base, "get_three") 64 | .unwrap() 65 | .as_func() 66 | .unwrap(); 67 | 68 | // Perform a call to see if everything works as expected 69 | let get_three_result = store.invoke_without_fuel(get_three, Vec::new()).unwrap(); 70 | assert_eq!(get_three_result, &[StoredValue::I32(3)],); 71 | } 72 | 73 | #[test_log::test] 74 | fn define_duplicate_extern_value() { 75 | const MODULE_WITH_EMPTY_FUNCTION: &str = r#"(module (func (export "foo") nop))"#; 76 | let wasm_bytes = wat::parse_str(MODULE_WITH_EMPTY_FUNCTION).unwrap(); 77 | let validation_info = validate(&wasm_bytes).unwrap(); 78 | 79 | let mut store = Store::new(()); 80 | 81 | let module = store 82 | .module_instantiate(&validation_info, Vec::new(), None) 83 | .unwrap() 84 | .module_addr; 85 | 86 | let foo_function = store.instance_export(module, "foo").unwrap(); 87 | 88 | { 89 | let mut linker = Linker::new(); 90 | 91 | linker 92 | .define_module_instance(&store, "bar".to_owned(), module) 93 | .unwrap(); 94 | assert_eq!( 95 | linker.define_module_instance(&store, "bar".to_owned(), module), 96 | Err(RuntimeError::DuplicateExternDefinition) 97 | ); 98 | } 99 | { 100 | let mut linker = Linker::new(); 101 | linker 102 | .define_module_instance(&store, "bar".to_owned(), module) 103 | .unwrap(); 104 | assert_eq!( 105 | linker.define("bar".to_owned(), "foo".to_owned(), foo_function), 106 | Err(RuntimeError::DuplicateExternDefinition) 107 | ); 108 | } 109 | { 110 | let mut linker = Linker::new(); 111 | linker 112 | .define("bar".to_owned(), "foo".to_owned(), foo_function) 113 | .unwrap(); 114 | assert_eq!( 115 | linker.define("bar".to_owned(), "foo".to_owned(), foo_function), 116 | Err(RuntimeError::DuplicateExternDefinition) 117 | ); 118 | } 119 | { 120 | let mut linker = Linker::new(); 121 | linker 122 | .define("bar".to_owned(), "foo".to_owned(), foo_function) 123 | .unwrap(); 124 | assert_eq!( 125 | linker.define_module_instance(&store, "bar".to_owned(), module), 126 | Err(RuntimeError::DuplicateExternDefinition) 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/core/reader/types/export.rs: -------------------------------------------------------------------------------- 1 | use alloc::borrow::ToOwned; 2 | use alloc::string::String; 3 | 4 | use crate::core::indices::{FuncIdx, GlobalIdx, MemIdx, TableIdx}; 5 | use crate::core::reader::{WasmReadable, WasmReader}; 6 | use crate::{ValidationError, ValidationInfo}; 7 | 8 | use super::ExternType; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct Export { 12 | #[allow(dead_code)] 13 | pub name: String, 14 | #[allow(dead_code)] 15 | pub desc: ExportDesc, 16 | } 17 | 18 | impl WasmReadable for Export { 19 | fn read(wasm: &mut WasmReader) -> Result { 20 | let name = wasm.read_name()?.to_owned(); 21 | let desc = ExportDesc::read(wasm)?; 22 | Ok(Export { name, desc }) 23 | } 24 | } 25 | 26 | #[derive(Debug, Clone)] 27 | #[allow(clippy::all)] 28 | // TODO: change enum labels from FuncIdx -> Func 29 | pub enum ExportDesc { 30 | #[allow(warnings)] 31 | FuncIdx(FuncIdx), 32 | #[allow(warnings)] 33 | TableIdx(TableIdx), 34 | #[allow(warnings)] 35 | MemIdx(MemIdx), 36 | #[allow(warnings)] 37 | GlobalIdx(GlobalIdx), 38 | } 39 | 40 | impl ExportDesc { 41 | /// returns the external type of `self` according to typing relation, 42 | /// taking `validation_info` as validation context C 43 | /// 44 | /// Note: This method may panic if `self` does not come from the given [`ValidationInfo`]. 45 | /// 46 | pub fn extern_type(&self, validation_info: &ValidationInfo) -> ExternType { 47 | match self { 48 | ExportDesc::FuncIdx(func_idx) => { 49 | let type_idx = validation_info 50 | .functions 51 | .get(*func_idx) 52 | .expect("func indices to always be valid if the validation info is correct"); 53 | let func_type = validation_info 54 | .types 55 | .get(*type_idx) 56 | .expect("type indices to always be valid if the validation info is correct"); 57 | // TODO ugly clone that should disappear when types are directly parsed from bytecode instead of vector copies 58 | ExternType::Func(func_type.clone()) 59 | } 60 | ExportDesc::TableIdx(table_idx) => ExternType::Table( 61 | *validation_info 62 | .tables 63 | .get(*table_idx) 64 | .expect("table indices to always be valid if the validation info is correct"), 65 | ), 66 | ExportDesc::MemIdx(mem_idx) => ExternType::Mem( 67 | *validation_info 68 | .memories 69 | .get(*mem_idx) 70 | .expect("mem indices to always be valid if the validation info is correct"), 71 | ), 72 | ExportDesc::GlobalIdx(global_idx) => ExternType::Global( 73 | validation_info 74 | .globals 75 | .get(*global_idx) 76 | .expect("global indices to always be valid if the validation info is correct") 77 | .ty, 78 | ), 79 | } 80 | } 81 | 82 | pub fn get_function_idx(&self) -> Option { 83 | match self { 84 | ExportDesc::FuncIdx(func_idx) => Some(*func_idx), 85 | _ => None, 86 | } 87 | } 88 | 89 | pub fn get_global_idx(&self) -> Option { 90 | match self { 91 | ExportDesc::GlobalIdx(global_idx) => Some(*global_idx), 92 | _ => None, 93 | } 94 | } 95 | 96 | pub fn get_memory_idx(&self) -> Option { 97 | match self { 98 | ExportDesc::MemIdx(mem_idx) => Some(*mem_idx), 99 | _ => None, 100 | } 101 | } 102 | 103 | pub fn get_table_idx(&self) -> Option { 104 | match self { 105 | ExportDesc::TableIdx(table_idx) => Some(*table_idx), 106 | _ => None, 107 | } 108 | } 109 | } 110 | 111 | impl WasmReadable for ExportDesc { 112 | fn read(wasm: &mut WasmReader) -> Result { 113 | let desc_id = wasm.read_u8()?; 114 | let desc_idx = wasm.read_var_u32()? as usize; 115 | 116 | let desc = match desc_id { 117 | 0x00 => ExportDesc::FuncIdx(desc_idx), 118 | 0x01 => ExportDesc::TableIdx(desc_idx), 119 | 0x02 => ExportDesc::MemIdx(desc_idx), 120 | 0x03 => ExportDesc::GlobalIdx(desc_idx), 121 | other => return Err(ValidationError::MalformedExportDescDiscriminator(other)), 122 | }; 123 | Ok(desc) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "devshell": { 4 | "inputs": { 5 | "nixpkgs": "nixpkgs" 6 | }, 7 | "locked": { 8 | "lastModified": 1741473158, 9 | "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", 10 | "owner": "numtide", 11 | "repo": "devshell", 12 | "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "devshell", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1722073938, 24 | "narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs_2": { 38 | "locked": { 39 | "lastModified": 1748162331, 40 | "narHash": "sha256-rqc2RKYTxP3tbjA+PB3VMRQNnjesrT0pEofXQTrMsS8=", 41 | "owner": "nixos", 42 | "repo": "nixpkgs", 43 | "rev": "7c43f080a7f28b2774f3b3f43234ca11661bf334", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "nixos", 48 | "ref": "nixos-25.05", 49 | "repo": "nixpkgs", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "devshell": "devshell", 56 | "nixpkgs": "nixpkgs_2", 57 | "rust-overlay": "rust-overlay", 58 | "treefmt-nix": "treefmt-nix", 59 | "typix": "typix", 60 | "utils": "utils" 61 | } 62 | }, 63 | "rust-overlay": { 64 | "inputs": { 65 | "nixpkgs": [ 66 | "nixpkgs" 67 | ] 68 | }, 69 | "locked": { 70 | "lastModified": 1748227081, 71 | "narHash": "sha256-RLnN7LBxhEdCJ6+rIL9sbhjBVDaR6jG377M/CLP/fmE=", 72 | "owner": "oxalica", 73 | "repo": "rust-overlay", 74 | "rev": "1cbe817fd8c64a9f77ba4d7861a4839b0b15983e", 75 | "type": "github" 76 | }, 77 | "original": { 78 | "owner": "oxalica", 79 | "repo": "rust-overlay", 80 | "type": "github" 81 | } 82 | }, 83 | "systems": { 84 | "locked": { 85 | "lastModified": 1681028828, 86 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 87 | "owner": "nix-systems", 88 | "repo": "default", 89 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 90 | "type": "github" 91 | }, 92 | "original": { 93 | "owner": "nix-systems", 94 | "repo": "default", 95 | "type": "github" 96 | } 97 | }, 98 | "treefmt-nix": { 99 | "inputs": { 100 | "nixpkgs": [ 101 | "nixpkgs" 102 | ] 103 | }, 104 | "locked": { 105 | "lastModified": 1748243702, 106 | "narHash": "sha256-9YzfeN8CB6SzNPyPm2XjRRqSixDopTapaRsnTpXUEY8=", 107 | "owner": "numtide", 108 | "repo": "treefmt-nix", 109 | "rev": "1f3f7b784643d488ba4bf315638b2b0a4c5fb007", 110 | "type": "github" 111 | }, 112 | "original": { 113 | "owner": "numtide", 114 | "repo": "treefmt-nix", 115 | "type": "github" 116 | } 117 | }, 118 | "typix": { 119 | "inputs": { 120 | "nixpkgs": [ 121 | "nixpkgs" 122 | ] 123 | }, 124 | "locked": { 125 | "lastModified": 1747903649, 126 | "narHash": "sha256-kQwFGFIC+sYsAkWZeXTzTQlo22btjh/nTCIazU2+WyM=", 127 | "owner": "loqusion", 128 | "repo": "typix", 129 | "rev": "72d9ed52872a7538c7429b72047873dd81c49193", 130 | "type": "github" 131 | }, 132 | "original": { 133 | "owner": "loqusion", 134 | "repo": "typix", 135 | "type": "github" 136 | } 137 | }, 138 | "utils": { 139 | "inputs": { 140 | "systems": "systems" 141 | }, 142 | "locked": { 143 | "lastModified": 1731533236, 144 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 145 | "ref": "refs/heads/main", 146 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 147 | "revCount": 102, 148 | "type": "git", 149 | "url": "https://github.com/numtide/flake-utils.git" 150 | }, 151 | "original": { 152 | "type": "git", 153 | "url": "https://github.com/numtide/flake-utils.git" 154 | } 155 | } 156 | }, 157 | "root": "root", 158 | "version": 7 159 | } 160 | -------------------------------------------------------------------------------- /src/validation/data.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | 3 | use crate::{ 4 | core::{ 5 | indices::MemIdx, 6 | reader::{ 7 | section_header::{SectionHeader, SectionTy}, 8 | types::{ 9 | data::{DataMode, DataModeActive, DataSegment}, 10 | global::GlobalType, 11 | }, 12 | WasmReader, 13 | }, 14 | }, 15 | read_constant_expression::read_constant_expression, 16 | validation_stack::ValidationStack, 17 | ValidationError, 18 | }; 19 | 20 | /// Validate the data section. 21 | pub(super) fn validate_data_section( 22 | wasm: &mut WasmReader, 23 | section_header: SectionHeader, 24 | imported_global_types: &[GlobalType], 25 | no_of_total_memories: usize, 26 | num_funcs: usize, 27 | ) -> Result, ValidationError> { 28 | assert_eq!(section_header.ty, SectionTy::Data); 29 | 30 | wasm.read_vec(|wasm| { 31 | use crate::{NumType, ValType}; 32 | let mode = wasm.read_var_u32()?; 33 | let data_sec: DataSegment = match mode { 34 | 0 => { 35 | // active { memory 0, offset e } 36 | trace!("Data section: active {{ memory 0, offset e }}"); 37 | 38 | if no_of_total_memories == 0 { 39 | return Err(ValidationError::InvalidMemIndex(0)); 40 | } 41 | 42 | let mut valid_stack = ValidationStack::new(); 43 | let (offset, _) = { 44 | read_constant_expression( 45 | wasm, 46 | &mut valid_stack, 47 | imported_global_types, 48 | num_funcs, 49 | )? 50 | }; 51 | 52 | valid_stack.assert_val_types(&[ValType::NumType(NumType::I32)], true)?; 53 | 54 | let byte_vec = wasm.read_vec(|el| el.read_u8())?; 55 | 56 | // WARN: we currently don't take into consideration how we act when we are dealing with globals here 57 | DataSegment { 58 | mode: DataMode::Active(DataModeActive { 59 | memory_idx: 0, 60 | offset, 61 | }), 62 | init: byte_vec, 63 | } 64 | } 65 | 1 => { 66 | // passive 67 | // A passive data segment's contents can be copied into a memory using the `memory.init` instruction 68 | trace!("Data section: passive"); 69 | DataSegment { 70 | mode: DataMode::Passive, 71 | init: wasm.read_vec(|el| el.read_u8())?, 72 | } 73 | } 74 | 2 => { 75 | trace!("Data section: active {{ memory x, offset e }}"); 76 | let mem_idx = wasm.read_var_u32()? as MemIdx; 77 | if mem_idx >= no_of_total_memories { 78 | return Err(crate::ValidationError::InvalidMemIndex(mem_idx)); 79 | } 80 | assert!( 81 | mem_idx == 0, 82 | "Memory index is not 0 - it's {mem_idx}! Multiple memories are NOT supported" 83 | ); 84 | 85 | let mut valid_stack = ValidationStack::new(); 86 | let (offset, _) = { 87 | read_constant_expression( 88 | wasm, 89 | &mut valid_stack, 90 | imported_global_types, 91 | num_funcs, 92 | )? 93 | }; 94 | 95 | valid_stack.assert_val_types(&[ValType::NumType(NumType::I32)], true)?; 96 | 97 | let byte_vec = wasm.read_vec(|el| el.read_u8())?; 98 | 99 | DataSegment { 100 | mode: DataMode::Active(DataModeActive { 101 | memory_idx: 0, 102 | offset, 103 | }), 104 | init: byte_vec, 105 | } 106 | // mode active { memory x, offset e } 107 | // this hasn't been yet implemented in wasm 108 | // as per docs: 109 | 110 | // https://webassembly.github.io/spec/core/binary/modules.html#data-section 111 | // The initial integer can be interpreted as a bitfield. Bit 0 indicates a passive segment, bit 1 indicates the presence of an explicit memory index for an active segment. 112 | // In the current version of WebAssembly, at most one memory may be defined or imported in a single module, so all valid active data segments have a memory value of 0 113 | } 114 | _ => unreachable!(), 115 | }; 116 | 117 | trace!("{:?}", data_sec.init); 118 | Ok(data_sec) 119 | }) 120 | } 121 | -------------------------------------------------------------------------------- /src/core/slotmap.rs: -------------------------------------------------------------------------------- 1 | use core::{marker::PhantomData, mem}; 2 | 3 | use alloc::vec::Vec; 4 | 5 | enum SlotContent { 6 | Unoccupied { prev_unoccupied: Option }, 7 | Occupied { item: T }, 8 | } 9 | 10 | struct Slot { 11 | generation: u64, 12 | content: SlotContent, 13 | } 14 | 15 | /// A contigious data structure that never shrinks, but keeps track of lazily deleted elements so that when a new item is inserted, the lazily deleted place is reused. 16 | /// Insertion, removal, and update are all of O(1) time complexity. Inserted elements can be (mutably) accessed or removed with the key returned when they were inserted. 17 | /// Note: might make a slot permanently unusable per `u64::MAX` insert calls during runtime. 18 | pub struct SlotMap { 19 | slots: Vec>, 20 | last_unoccupied: Option, 21 | } 22 | 23 | impl Default for SlotMap { 24 | fn default() -> Self { 25 | Self { 26 | slots: Vec::new(), 27 | last_unoccupied: None, 28 | } 29 | } 30 | } 31 | 32 | pub struct SlotMapKey { 33 | index: usize, 34 | generation: u64, 35 | phantom: PhantomData, 36 | } 37 | 38 | impl SlotMap { 39 | pub fn insert(&mut self, item: T) -> SlotMapKey { 40 | match self.last_unoccupied { 41 | Some(last_unoccupied) => { 42 | let slot = &mut self.slots[last_unoccupied]; 43 | let SlotContent::Unoccupied { prev_unoccupied } = slot.content else { 44 | unreachable!("last unoccupied slot in slotmap must be unoccupied") 45 | }; 46 | self.last_unoccupied = prev_unoccupied; 47 | 48 | slot.content = SlotContent::Occupied { item }; 49 | SlotMapKey { 50 | index: last_unoccupied, 51 | generation: slot.generation, 52 | phantom: PhantomData, 53 | } 54 | } 55 | None => { 56 | let index = self.slots.len(); 57 | let generation = 0; 58 | self.slots.push(Slot { 59 | generation, 60 | content: SlotContent::Occupied { item }, 61 | }); 62 | SlotMapKey { 63 | index, 64 | generation, 65 | phantom: PhantomData, 66 | } 67 | } 68 | } 69 | } 70 | 71 | #[allow(unused)] 72 | pub fn get(&self, key: &SlotMapKey) -> Option<&T> { 73 | let slot = self.slots.get(key.index)?; 74 | if slot.generation != key.generation { 75 | return None; 76 | } 77 | match &slot.content { 78 | SlotContent::Occupied { item } => Some(item), 79 | SlotContent::Unoccupied { .. } => None, 80 | } 81 | } 82 | 83 | pub fn get_mut(&mut self, key: &SlotMapKey) -> Option<&mut T> { 84 | let slot = self.slots.get_mut(key.index)?; 85 | if slot.generation != key.generation { 86 | return None; 87 | } 88 | match &mut slot.content { 89 | SlotContent::Occupied { item } => Some(item), 90 | SlotContent::Unoccupied { .. } => None, 91 | } 92 | } 93 | 94 | pub fn remove(&mut self, key: &SlotMapKey) -> Option { 95 | let slot = self.slots.get_mut(key.index)?; 96 | if slot.generation != key.generation 97 | || matches!(slot.content, SlotContent::Unoccupied { .. }) 98 | { 99 | return None; 100 | } 101 | 102 | let new_slot = if let Some(generation) = slot.generation.checked_add(1) { 103 | let prev_unoccupied = self.last_unoccupied; 104 | self.last_unoccupied = Some(key.index); 105 | Slot { 106 | generation, 107 | content: SlotContent::Unoccupied { prev_unoccupied }, 108 | } 109 | } else { 110 | // no self.last_unoccupied update here, permanently make slot unusable if its generation overflows. 111 | Slot { 112 | generation: 0, 113 | content: SlotContent::Unoccupied { 114 | prev_unoccupied: None, 115 | }, 116 | } 117 | }; 118 | let previous_slot = mem::replace(slot, new_slot); 119 | 120 | let SlotContent::Occupied { item } = previous_slot.content else { 121 | unreachable!("slot was full") 122 | }; 123 | 124 | Some(item) 125 | } 126 | } 127 | 128 | #[test] 129 | fn test() { 130 | let mut slotmap = SlotMap::::default(); 131 | let key = slotmap.insert(5); 132 | assert_eq!(slotmap.get(&key), Some(&5)); 133 | slotmap.remove(&key); 134 | assert_eq!(slotmap.get(&key), None); 135 | let key2 = slotmap.insert(10); 136 | assert_eq!(slotmap.get(&key), None); 137 | assert_eq!(slotmap.get(&key2), Some(&10)); 138 | let n = slotmap.get_mut(&key2).unwrap(); 139 | *n = 42; 140 | assert_eq!(slotmap.get(&key2), Some(&42)); 141 | } 142 | -------------------------------------------------------------------------------- /tests/table_get.rs: -------------------------------------------------------------------------------- 1 | /* 2 | # This file incorporates code from the WebAssembly testsuite, originally 3 | # available at https://github.com/WebAssembly/testsuite. 4 | # 5 | # The original code is licensed under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | */ 17 | 18 | use wasm::{ 19 | checked::StoredRefFunc, interop::RefExtern, validate, value::ExternAddr, RuntimeError, Store, 20 | TrapError, 21 | }; 22 | 23 | #[test_log::test] 24 | fn table_funcref_test() { 25 | let w = r#" 26 | (module 27 | (table $t2 2 externref) 28 | (table $t3 3 funcref) 29 | (elem (table $t3) (i32.const 1) func $dummy) 30 | (func $dummy) 31 | 32 | (func (export "init") (param $r externref) 33 | (table.set $t2 (i32.const 1) (local.get $r)) 34 | (table.set $t3 (i32.const 2) (table.get $t3 (i32.const 1))) 35 | ) 36 | 37 | (func (export "get-externref") (param $i i32) (result externref) 38 | (table.get (local.get $i)) 39 | ) 40 | (func $f3 (export "get-funcref") (param $i i32) (result funcref) 41 | (table.get $t3 (local.get $i)) 42 | ) 43 | 44 | (func (export "is_null-funcref") (param $i i32) (result i32) 45 | (ref.is_null (call $f3 (local.get $i))) 46 | ) 47 | ) 48 | "#; 49 | let wasm_bytes = wat::parse_str(w).unwrap(); 50 | let validation_info = validate(&wasm_bytes).unwrap(); 51 | let mut store = Store::new(()); 52 | let module = store 53 | .module_instantiate(&validation_info, Vec::new(), None) 54 | .unwrap() 55 | .module_addr; 56 | 57 | let init = store 58 | .instance_export(module, "init") 59 | .unwrap() 60 | .as_func() 61 | .unwrap(); 62 | let get_externref = store 63 | .instance_export(module, "get-externref") 64 | .unwrap() 65 | .as_func() 66 | .unwrap(); 67 | let get_funcref = store 68 | .instance_export(module, "get-funcref") 69 | .unwrap() 70 | .as_func() 71 | .unwrap(); 72 | let is_null_funcref = store 73 | .instance_export(module, "is_null-funcref") 74 | .unwrap() 75 | .as_func() 76 | .unwrap(); 77 | 78 | store 79 | .invoke_typed_without_fuel::(init, RefExtern(Some(ExternAddr(1)))) 80 | .unwrap(); 81 | 82 | assert_eq!( 83 | store.invoke_typed_without_fuel(get_externref, 0), 84 | Ok(RefExtern(None)) 85 | ); 86 | assert_eq!( 87 | store.invoke_typed_without_fuel(get_externref, 1), 88 | Ok(RefExtern(Some(ExternAddr(1)))) 89 | ); 90 | assert_eq!( 91 | store.invoke_typed_without_fuel(get_funcref, 0), 92 | Ok(StoredRefFunc(None)) 93 | ); 94 | assert_eq!(store.invoke_typed_without_fuel(is_null_funcref, 1), Ok(0)); 95 | assert_eq!(store.invoke_typed_without_fuel(is_null_funcref, 2), Ok(0)); 96 | 97 | assert_eq!( 98 | store 99 | .invoke_typed_without_fuel::(get_externref, 2) 100 | .err(), 101 | Some(RuntimeError::Trap( 102 | TrapError::TableOrElementAccessOutOfBounds 103 | )) 104 | ); 105 | assert_eq!( 106 | store 107 | .invoke_typed_without_fuel::(get_funcref, 3) 108 | .err(), 109 | Some(RuntimeError::Trap( 110 | TrapError::TableOrElementAccessOutOfBounds 111 | )) 112 | ); 113 | assert_eq!( 114 | store 115 | .invoke_typed_without_fuel::(get_externref, -1) 116 | .err(), 117 | Some(RuntimeError::Trap( 118 | TrapError::TableOrElementAccessOutOfBounds 119 | )) 120 | ); 121 | assert_eq!( 122 | store 123 | .invoke_typed_without_fuel::(get_funcref, -1) 124 | .err(), 125 | Some(RuntimeError::Trap( 126 | TrapError::TableOrElementAccessOutOfBounds 127 | )) 128 | ); 129 | } 130 | 131 | #[test_log::test] 132 | fn table_type_error_test() { 133 | let invalid_modules = vec![ 134 | r#"(module (table $t 10 funcref) (func $type-index-empty-vs-i32 (result funcref) (table.get $t)))"#, 135 | r#"(module (table $t 10 funcref) (func $type-index-f32-vs-i32 (result funcref) (table.get $t (f32.const 1))))"#, 136 | r#"(module (table $t 10 funcref) (func $type-result-funcref-vs-empty (table.get $t (i32.const 0))))"#, 137 | r#"(module (table $t 10 funcref) (func $type-result-funcref-vs-funcref (result externref) (table.get $t (i32.const 1))))"#, 138 | r#"(module (table $t1 1 funcref) (table $t2 1 externref) (func $type-result-externref-vs-funcref-multi (result funcref) (table.get $t2 (i32.const 0))))"#, 139 | ]; 140 | 141 | for module in invalid_modules { 142 | let wasm_bytes = wat::parse_str(module).unwrap(); 143 | let result = validate(&wasm_bytes); 144 | assert!( 145 | result.is_err(), 146 | "Result `{result:?}` was expected to be `Err`, but it is not." 147 | ); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/execution/config.rs: -------------------------------------------------------------------------------- 1 | /// Trait that allows user specified configuration for various items during interpretation. Additionally, the types 2 | /// implementing this trait can act as custom user data within an interpreter instance, passed along to each method of 3 | /// this trait and host functions whenever they are invoked. 4 | /// 5 | /// The default implementation of all trait methods have the least overhead, i. e. most can be optimized out fully. 6 | // It must always be checked that there is no additional performance penalty for the default config! 7 | pub trait Config { 8 | /// Maximum number of values in the value stack 9 | const MAX_VALUE_STACK_SIZE: usize = 0xf0000; // 64 Kibi-Values 10 | 11 | /// Maximum number of cascading function invocations 12 | const MAX_CALL_STACK_SIZE: usize = 0x1000; // 4 Kibi-Functions 13 | 14 | /// A hook which is called before every wasm instruction 15 | /// 16 | /// This allows the most intricate insight into the interpreters behavior, at the cost of a 17 | /// hefty performance penalty 18 | #[allow(unused_variables)] 19 | #[inline(always)] 20 | fn instruction_hook(&mut self, bytecode: &[u8], pc: usize) {} 21 | 22 | /// Amount of fuel to be deducted when a single byte `instr` is hit. The cost corresponding to `UNREACHABLE` and 23 | /// `END` instructions and other bytes that do not correspond to any Wasm instruction are ignored. 24 | // It must always be checked that the calls to this method fold into a constant if it is just a match statement that 25 | // yields constants. 26 | #[inline(always)] 27 | fn get_flat_cost(_instr: u8) -> u32 { 28 | 1 29 | } 30 | 31 | /// Amount of fuel to be deducted when a multi-byte instruction that starts with the byte 0xFC is hit. This method 32 | /// should return the cost of an instruction obtained by prepending 0xFC to of an unsigned 32-bit LEB 33 | /// representation of `instr`. Multi-byte sequences obtained this way that do not correspond to any Wasm instruction 34 | /// are ignored. 35 | // It must always be checked that the calls to this method fold into a constant if it is just a match statement that 36 | // yields constants. 37 | #[inline(always)] 38 | fn get_fc_extension_flat_cost(_instr: u32) -> u32 { 39 | 1 40 | } 41 | 42 | /// Amount of fuel to be deducted when a multi-byte instruction that starts with the byte 0xFD is hit. This method 43 | /// should return the cost of an instruction obtained by prepending 0xFD to of an unsigned 32-bit LEB 44 | /// representation of `instr`. Multi-byte sequences obtained this way that do not correspond to any Wasm instruction 45 | /// are ignored. 46 | // It must always be checked that the calls to this method fold into a constant if it is just a match statement that 47 | // yields constants. 48 | #[inline(always)] 49 | fn get_fd_extension_flat_cost(_instr: u32) -> u32 { 50 | 1 51 | } 52 | 53 | /// Amount of fuel to be deducted per element of a single byte instruction `instr` that executes in asymptotically 54 | /// linear time with respect to one of the values it pops from the stack. 55 | /// 56 | /// In Wasm 2.0 specification, this applies to the following instructions: 57 | /// - `MEMORY.GROW` of type `[n: i32] -> [i32]` 58 | /// 59 | /// The cost of the instruction is calculated as `cost := get_flat_cost(instr) + n*get_cost_per_element(instr)`. 60 | /// where `n` is the stack value marked in the instruction type signature above. Other instructions and bytes that 61 | /// do not correspond to any instruction are ignored. 62 | // It must always be checked that the calls to this method fold into a constant if it is just a match statement that 63 | // yields constants. 64 | #[inline(always)] 65 | fn get_cost_per_element(_instr: u8) -> u32 { 66 | 0 67 | } 68 | 69 | /// Amount of fuel to be deducted per element of a multi-byte instruction that starts with the byte 0xFC, 70 | /// which executes in asymptotically linear time with respect to one of the values it pops from the stack. This 71 | /// method should return the cost of an instruction obtained by prepending 0xFD to of an unsigned 32-bit LEB 72 | /// representation of `instr`. Multi-byte sequences obtained this way that do not correspond to any Wasm instruction 73 | /// are ignored. 74 | /// 75 | /// In Wasm 2.0 specification, this applies to the following instructions: 76 | /// - `MEMORY.INIT x` of type `[d:i32 s: i32 n: i32] -> []` 77 | /// - `MEMORY.FILL` of type `[d: i32 val: i32 n: i32] -> []` 78 | /// - `MEMORY.COPY` of type `[d: i32 s: i32 n: i32] -> []` 79 | /// - `TABLE.GROW x` of type `[val: ref n: i32] -> [i32]` 80 | /// - `TABLE.INIT x y` of type `[d: i32 s: i32 n: i32] -> []` 81 | /// - `TABLE.FILL x` of type `[i: i32 val: ref n: i32] -> []` 82 | /// - `TABLE.COPY x y` of type `[d: i32 s: i32 n: i32] -> []` 83 | /// 84 | /// The cost of the instruction is calculated as `cost := get_flat_cost(instr) + n*get_cost_per_element(instr)`. 85 | /// where `n` is the stack value marked in the instruction type signature above. Other instructions and multi-byte 86 | /// sequences that do not correspond to any instruction are ignored. 87 | // It must always be checked that the calls to this method fold into a constant if it is just a match statement that 88 | // yields constants. 89 | #[inline(always)] 90 | fn get_fc_extension_cost_per_element(_instr: u32) -> u32 { 91 | 0 92 | } 93 | } 94 | 95 | /// Default implementation of the interpreter configuration, with all hooks empty 96 | impl Config for () {} 97 | -------------------------------------------------------------------------------- /src/execution/const_interpreter_loop.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | addrs::ModuleAddr, 3 | assert_validated::UnwrapValidatedExt, 4 | config::Config, 5 | core::{ 6 | indices::GlobalIdx, 7 | reader::{span::Span, WasmReadable, WasmReader}, 8 | }, 9 | unreachable_validated, 10 | value::{self, Ref}, 11 | value_stack::Stack, 12 | RefType, RuntimeError, Store, Value, 13 | }; 14 | 15 | // TODO update this documentation 16 | /// Execute a previosly-validated constant expression. These type of expressions are used for initializing global 17 | /// variables, data and element segments. 18 | /// 19 | /// # Arguments 20 | /// TODO 21 | /// 22 | /// # Safety 23 | /// This function assumes that the expression has been validated. Passing unvalidated code will likely result in a 24 | /// panic, or undefined behaviour. 25 | // TODO this signature might change to support hooks or match the spec better 26 | pub(crate) fn run_const( 27 | wasm: &mut WasmReader, 28 | stack: &mut Stack, 29 | module: ModuleAddr, 30 | store: &Store, 31 | ) -> Result<(), RuntimeError> { 32 | use crate::core::reader::types::opcode::*; 33 | loop { 34 | let first_instr_byte = wasm.read_u8().unwrap_validated(); 35 | 36 | #[cfg(debug_assertions)] 37 | crate::core::utils::print_beautiful_instruction_name_1_byte(first_instr_byte, wasm.pc); 38 | 39 | #[cfg(not(debug_assertions))] 40 | trace!("Read instruction byte {first_instr_byte:#04X?} ({first_instr_byte}) at wasm_binary[{}]", wasm.pc); 41 | 42 | match first_instr_byte { 43 | END => { 44 | trace!("Constant instruction: END"); 45 | break; 46 | } 47 | GLOBAL_GET => { 48 | let global_idx = wasm.read_var_u32().unwrap_validated() as GlobalIdx; 49 | 50 | //TODO replace double indirection 51 | let global = store 52 | .globals 53 | .get(store.modules.get(module).global_addrs[global_idx]); 54 | 55 | trace!( 56 | "Constant instruction: global.get [{global_idx}] -> [{:?}]", 57 | global 58 | ); 59 | stack.push_value::(global.value)?; 60 | } 61 | I32_CONST => { 62 | let constant = wasm.read_var_i32().unwrap_validated(); 63 | trace!("Constant instruction: i32.const [] -> [{constant}]"); 64 | stack.push_value::(constant.into())?; 65 | } 66 | F32_CONST => { 67 | let constant = value::F32::from_bits(wasm.read_f32().unwrap_validated()); 68 | trace!("Constanting instruction: f32.const [] -> [{constant}]"); 69 | stack.push_value::(constant.into())?; 70 | } 71 | F64_CONST => { 72 | let constant = value::F64::from_bits(wasm.read_f64().unwrap_validated()); 73 | trace!("Constanting instruction: f64.const [] -> [{constant}]"); 74 | stack.push_value::(constant.into())?; 75 | } 76 | I64_CONST => { 77 | let constant = wasm.read_var_i64().unwrap_validated(); 78 | trace!("Constant instruction: i64.const [] -> [{constant}]"); 79 | stack.push_value::(constant.into())?; 80 | } 81 | REF_NULL => { 82 | let reftype = RefType::read(wasm).unwrap_validated(); 83 | 84 | stack.push_value::(Value::Ref(Ref::Null(reftype)))?; 85 | trace!("Instruction: ref.null '{:?}' -> [{:?}]", reftype, reftype); 86 | } 87 | REF_FUNC => { 88 | // we already checked for the func_idx to be in bounds during validation 89 | let func_idx = wasm.read_var_u32().unwrap_validated() as usize; 90 | let func_addr = *store 91 | .modules 92 | .get(module) 93 | .func_addrs 94 | .get(func_idx) 95 | .unwrap_validated(); 96 | stack.push_value::(Value::Ref(Ref::Func(func_addr)))?; 97 | } 98 | 99 | FD_EXTENSIONS => { 100 | use crate::core::reader::types::opcode::fd_extensions::*; 101 | 102 | match wasm.read_var_u32().unwrap_validated() { 103 | V128_CONST => { 104 | let mut data = [0; 16]; 105 | for byte_ref in &mut data { 106 | *byte_ref = wasm.read_u8().unwrap_validated(); 107 | } 108 | 109 | stack.push_value::(Value::V128(data))?; 110 | } 111 | 0x00..=0x0B | 0x0D.. => unreachable_validated!(), 112 | } 113 | } 114 | 115 | 0x00..=0x0A 116 | | 0x0C..=0x22 117 | | 0x24..=0x40 118 | | 0x45..=0xBF 119 | | 0xC0..=0xCF 120 | | 0xD1 121 | | 0xD3..=0xFC 122 | | 0xFE..=0xFF => { 123 | unreachable_validated!(); 124 | } 125 | } 126 | } 127 | Ok(()) 128 | } 129 | 130 | pub(crate) fn run_const_span( 131 | wasm: &[u8], 132 | span: &Span, 133 | module: ModuleAddr, 134 | store: &Store, 135 | ) -> Result, RuntimeError> { 136 | let mut wasm = WasmReader::new(wasm); 137 | 138 | wasm.move_start_to(*span).unwrap_validated(); 139 | 140 | let mut stack = Stack::new(); 141 | run_const(&mut wasm, &mut stack, module, store)?; 142 | 143 | Ok(stack.peek_value()) 144 | } 145 | -------------------------------------------------------------------------------- /tests/specification/mod.rs: -------------------------------------------------------------------------------- 1 | use ci_reports::CIFullReport; 2 | use envconfig::Envconfig; 3 | use regex::Regex; 4 | use reports::{AssertReport, ScriptError}; 5 | use std::fmt::Write as _; 6 | use std::path::Path; 7 | use std::process::ExitCode; 8 | 9 | mod ci_reports; 10 | mod files; 11 | mod reports; 12 | mod run; 13 | mod test_errors; 14 | 15 | #[derive(Envconfig)] 16 | pub struct GlobalConfig { 17 | /// A regex that acts as an allowlist filter for tests. 18 | /// By default all tests are allowed. 19 | #[envconfig(default = ".*")] 20 | pub allow_test_pattern: Regex, 21 | 22 | /// A regex that acts as a blocklist filter for tests. 23 | /// By default all `simd_*` and `proposals` tests are blocked. 24 | /// To not block anything use: `^$` 25 | #[envconfig(default = r"^(proposals|names\.wast)$")] 26 | pub block_test_pattern: Regex, 27 | 28 | /// This makes the testsuite runner re-enable the panic hook during all interpreter calls, resulting in the printing of panic info on every interpreter panic. 29 | #[envconfig(default = "false")] 30 | pub reenable_panic_hook: bool, 31 | 32 | /// This makes the testsuite runner exit with FAILURE with any failing test 33 | #[envconfig(default = "true")] 34 | pub fail_if_any_test_fails: bool, 35 | } 36 | 37 | lazy_static::lazy_static! { 38 | pub static ref ENV_CONFIG: GlobalConfig = GlobalConfig::init_from_env().expect("valid environment variables"); 39 | } 40 | 41 | #[test_log::test] 42 | pub fn spec_tests() -> ExitCode { 43 | // Load environment variables 44 | let _ = *ENV_CONFIG; 45 | 46 | // Edit this to ignore or only run specific tests 47 | let file_name_filter = |file_name: &str| { 48 | ENV_CONFIG.allow_test_pattern.is_match(file_name) 49 | && !ENV_CONFIG.block_test_pattern.is_match(file_name) 50 | }; 51 | 52 | let paths = files::find_wast_files( 53 | Path::new("./tests/specification/testsuite/"), 54 | file_name_filter, 55 | ) 56 | .expect("Failed to open testsuite directory"); 57 | 58 | assert!( 59 | !paths.is_empty(), 60 | "No paths were found, please check if the git submodules are correctly fetched" 61 | ); 62 | 63 | // Some statistics about the reports 64 | let mut num_failures = 0; 65 | let mut num_script_errors = 0; 66 | 67 | // Used for padding of filenames with spaces later 68 | let mut longest_filename_len: usize = 0; 69 | 70 | let reports: Vec> = paths 71 | .into_iter() 72 | .map(|path| run::run_spec_test(path.to_str().unwrap())) 73 | .inspect(|report| { 74 | match report { 75 | Ok(assert_report) => { 76 | longest_filename_len = longest_filename_len.max(assert_report.filename.len()); 77 | 78 | if assert_report.has_errors() { 79 | num_failures += 1; 80 | } 81 | } 82 | Err(_) => { 83 | num_script_errors += 1; 84 | } 85 | }; 86 | }) 87 | .collect(); 88 | 89 | // Calculate another required statistic 90 | let num_successes = reports.len() - num_script_errors - num_failures; 91 | 92 | // Collect all reports without errors along with some statistic 93 | let mut successful_mini_tests = 0; 94 | let mut total_mini_tests = 0; 95 | let mut assert_reports: Vec<&reports::AssertReport> = reports 96 | .iter() 97 | .filter_map(|r| r.as_ref().ok()) 98 | .inspect(|assert_report| { 99 | successful_mini_tests += assert_report.passed_asserts(); 100 | total_mini_tests += assert_report.total_asserts(); 101 | }) 102 | .collect(); 103 | 104 | // Sort all reports without errors for displaying it to the user later 105 | assert_reports.sort_by(|a, b| { 106 | b.percentage_asserts_passed() 107 | .total_cmp(&a.percentage_asserts_passed()) 108 | }); 109 | 110 | let mut final_status: String = String::new(); 111 | // Printing success rate per file for those that did NOT error out when compiling 112 | for report in assert_reports { 113 | writeln!(final_status, 114 | "Report for {:filename_width$}: Tests: {:passed_width$} Passed, {:failed_width$} Failed --- {:percentage_width$.2}%", 115 | report.filename, 116 | report.passed_asserts(), 117 | report.failed_asserts(), 118 | report.percentage_asserts_passed(), 119 | filename_width = longest_filename_len + 1, 120 | passed_width = 7, 121 | failed_width = 7, 122 | percentage_width = 6 123 | ).expect("writing into strings to never fail"); 124 | 125 | if report.passed_asserts() < report.total_asserts() { 126 | println!("{report}"); 127 | } 128 | } 129 | 130 | println!("{final_status}"); 131 | 132 | println!( 133 | "\nReport for {:filename_width$}: Tests: {:passed_width$} Passed, {:failed_width$} Failed --- {:percentage_width$.2}%\n\n", 134 | "all of the above", 135 | successful_mini_tests, 136 | total_mini_tests - successful_mini_tests, 137 | if total_mini_tests == 0 { 0.0 } else {(successful_mini_tests as f64) * 100.0 / (total_mini_tests as f64)}, 138 | filename_width = longest_filename_len + 1, 139 | passed_width = 7, 140 | failed_width = 7, 141 | percentage_width = 6 142 | ); 143 | 144 | println!( 145 | "Tests: {num_successes} Passed, {num_failures} Failed, {num_script_errors} Compilation Errors" 146 | ); 147 | 148 | // Optional: We need to save the result to a file for CI Regression Analysis 149 | if std::option_env!("TESTSUITE_SAVE").is_some() { 150 | let ci_report = CIFullReport::new(reports); 151 | let output_file = std::fs::File::create("./testsuite_results.json").unwrap(); 152 | 153 | serde_json::to_writer_pretty(output_file, &ci_report).unwrap(); 154 | } 155 | 156 | if ENV_CONFIG.fail_if_any_test_fails && (num_failures != 0 || num_script_errors != 0) { 157 | return ExitCode::FAILURE; 158 | } 159 | ExitCode::SUCCESS 160 | } 161 | -------------------------------------------------------------------------------- /tests/specification/reports.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use wasm::{RuntimeError, TrapError}; 4 | 5 | use super::test_errors::AssertEqError; 6 | 7 | pub struct AssertOutcome { 8 | pub line_number: u32, 9 | pub command: String, 10 | pub maybe_error: Option, 11 | } 12 | 13 | #[derive(thiserror::Error, Debug)] 14 | pub enum WastError { 15 | #[error("Panic: {}", .0.downcast_ref::<&str>().unwrap_or(&"Unknown panic"))] 16 | Panic(Box), 17 | #[error("{0}")] 18 | WasmError(wasm::ValidationError), 19 | #[error("{0}")] 20 | WasmRuntimeError(wasm::RuntimeError), 21 | #[error("{0}")] 22 | AssertEqualFailed(#[from] AssertEqError), 23 | #[error("Module validated and instantiated successfully, when it shouldn't have")] 24 | AssertInvalidButValid, 25 | #[error("Module linked successfully, when it shouldn't have")] 26 | AssertUnlinkableButLinked, 27 | #[error("'assert_exhaustion': Expected '{expected}' - Actual: '{}'", actual.as_ref() 28 | .map(|actual| format!("{actual}")) 29 | .unwrap_or_else(|| "---".to_owned()) 30 | )] 31 | AssertExhaustionButDidNotExhaust { 32 | expected: String, 33 | actual: Option, 34 | }, 35 | #[error("'assert_trap': Expected '{expected}' - Actual: '{}'", actual.as_ref() 36 | .map(|actual| format!("{actual}")) 37 | .unwrap_or_else(|| "---".to_owned()) 38 | )] 39 | AssertTrapButTrapWasIncorrect { 40 | expected: String, 41 | actual: Option, 42 | }, 43 | #[error("{0}")] 44 | Wast(#[from] wast::Error), 45 | #[error("Runtime error not represented in WAST")] 46 | UnrepresentedRuntimeError, 47 | #[error("{0}")] 48 | Io(#[from] std::io::Error), 49 | #[error("Some directive either referenced a non-existing Wasm module by its id or it did not specify an id at all and there was no other module defined prior to this directive.")] 50 | UnknownModuleReferenced, 51 | #[error("An directive referenced a non-existing function export")] 52 | UnknownFunctionReferenced, 53 | #[error("An directive referenced a non-existing global export")] 54 | UnknownGlobalReferenced, 55 | } 56 | 57 | impl From for WastError { 58 | fn from(value: wasm::ValidationError) -> Self { 59 | Self::WasmError(value) 60 | } 61 | } 62 | 63 | impl From for WastError { 64 | fn from(value: wasm::RuntimeError) -> Self { 65 | Self::WasmRuntimeError(value) 66 | } 67 | } 68 | 69 | /// Wast script executed successfully. The outcomes of asserts (pass/fail) are 70 | /// stored here. 71 | pub struct AssertReport { 72 | pub filename: String, 73 | pub results: Vec, 74 | } 75 | 76 | impl AssertReport { 77 | pub fn new(filename: &str) -> Self { 78 | Self { 79 | filename: filename.to_string(), 80 | results: Vec::new(), 81 | } 82 | } 83 | 84 | pub fn has_errors(&self) -> bool { 85 | self.results.iter().any(|r| r.maybe_error.is_some()) 86 | } 87 | 88 | pub fn total_asserts(&self) -> u32 { 89 | self.results.len() as u32 90 | } 91 | 92 | pub fn passed_asserts(&self) -> u32 { 93 | self.results 94 | .iter() 95 | .filter(|el| el.maybe_error.is_none()) 96 | .count() as u32 97 | } 98 | 99 | pub fn failed_asserts(&self) -> u32 { 100 | self.total_asserts() - self.passed_asserts() 101 | } 102 | 103 | pub fn percentage_asserts_passed(&self) -> f32 { 104 | if self.total_asserts() == 0 { 105 | 0.0 106 | } else { 107 | (self.passed_asserts() as f32) * 100.0 / (self.total_asserts() as f32) 108 | } 109 | } 110 | } 111 | 112 | impl std::fmt::Display for AssertReport { 113 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 114 | for elem in &self.results { 115 | match &elem.maybe_error { 116 | None => { 117 | writeln!( 118 | f, 119 | "✅ {}:{} -> {}", 120 | self.filename, 121 | if elem.line_number == u32::MAX { 122 | "?".to_string() 123 | } else { 124 | elem.line_number.to_string() 125 | }, 126 | elem.command 127 | )?; 128 | } 129 | Some(error) => { 130 | writeln!( 131 | f, 132 | "❌ {}:{} -> {}", 133 | self.filename, elem.line_number, elem.command 134 | )?; 135 | writeln!(f, " Error: {error}")?; 136 | } 137 | } 138 | } 139 | 140 | Ok(()) 141 | } 142 | } 143 | 144 | /// An error originating from within the WAST Script. If a non-assert directive 145 | /// fails, a ScriptError will be raised. 146 | pub struct ScriptError { 147 | pub filename: String, 148 | /// Boxed because of struct size 149 | pub error: Box, 150 | pub context: String, 151 | #[allow(unused)] 152 | pub line_number: Option, 153 | #[allow(unused)] 154 | pub command: Option, 155 | } 156 | 157 | impl ScriptError { 158 | pub fn new( 159 | filename: &str, 160 | error: WastError, 161 | context: &str, 162 | line_number: u32, 163 | command: &str, 164 | ) -> Self { 165 | Self { 166 | filename: filename.to_string(), 167 | error: Box::new(error), 168 | context: context.to_string(), 169 | line_number: Some(line_number), 170 | command: Some(command.to_string()), 171 | } 172 | } 173 | 174 | pub fn new_lineless(filename: &str, error: WastError, context: &str) -> Self { 175 | Self { 176 | filename: filename.to_string(), 177 | error: Box::new(error), 178 | context: context.to_string(), 179 | line_number: None, 180 | command: None, 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /tests/globals.rs: -------------------------------------------------------------------------------- 1 | use wasm::{checked::StoredValue, GlobalType, NumType, ValType}; 2 | 3 | /// The WASM program has one mutable global initialized with a constant 3. 4 | /// It exports two methods: 5 | /// - Setting the global's value and returning its previous value 6 | /// - Getting the global's current value 7 | #[test_log::test] 8 | fn valid_global() { 9 | use wasm::{validate, Store}; 10 | 11 | let wat = r#" 12 | (module 13 | (global $my_global (mut i32) 14 | i32.const 5 15 | ) 16 | 17 | ;; Set global to a value and return the previous one 18 | (func (export "set") (param i32) (result i32) 19 | global.get $my_global 20 | local.get 0 21 | global.set $my_global) 22 | 23 | ;; Returns the global's current value 24 | (func (export "get") (result i32) 25 | global.get $my_global) 26 | ) 27 | "#; 28 | let wasm_bytes = wat::parse_str(wat).unwrap(); 29 | 30 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 31 | let mut store = Store::new(()); 32 | let module = store 33 | .module_instantiate(&validation_info, Vec::new(), None) 34 | .unwrap() 35 | .module_addr; 36 | 37 | let set = store 38 | .instance_export(module, "set") 39 | .unwrap() 40 | .as_func() 41 | .unwrap(); 42 | 43 | let get = store 44 | .instance_export(module, "get") 45 | .unwrap() 46 | .as_func() 47 | .unwrap(); 48 | 49 | // Set global to 17. 5 is returned as previous (default) value. 50 | assert_eq!(5, store.invoke_typed_without_fuel(set, 17).unwrap()); 51 | 52 | // Now 17 will be returned when getting the global 53 | assert_eq!(17, store.invoke_typed_without_fuel(get, ()).unwrap()); 54 | } 55 | 56 | #[test_log::test] 57 | fn global_invalid_value_stack() { 58 | use wasm::validate; 59 | 60 | let wat = r#" 61 | (module 62 | (global $my_global (mut i32) 63 | i32.const 2 64 | i32.const 2 65 | i32.const 2 66 | i32.const 2 67 | i32.const 2 68 | i32.const 2 69 | i32.const 3 70 | ) 71 | ) 72 | "#; 73 | let wasm_bytes = wat::parse_str(wat).unwrap(); 74 | 75 | if validate(&wasm_bytes).is_ok() { 76 | panic!("validation succeeded") 77 | } 78 | } 79 | 80 | #[ignore = "not yet implemented"] 81 | #[test_log::test] 82 | fn imported_globals() { 83 | use wasm::{validate, Store}; 84 | 85 | let wat = r#" 86 | (module 87 | (import "env" "global" (global $my_global (mut i32))) 88 | 89 | ;; Set global to a value and return the previous one 90 | (func (export "set") (param i32) (result i32) 91 | global.get $my_global 92 | local.get 0 93 | global.set $my_global) 94 | 95 | ;; Returns the global's current value 96 | (func (export "get") (result i32) 97 | global.get $my_global) 98 | ) 99 | "#; 100 | let wasm_bytes = wat::parse_str(wat).unwrap(); 101 | 102 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 103 | let mut store = Store::new(()); 104 | let module = store 105 | .module_instantiate(&validation_info, Vec::new(), None) 106 | .unwrap() 107 | .module_addr; 108 | 109 | let set = store 110 | .instance_export(module, "set") 111 | .unwrap() 112 | .as_func() 113 | .unwrap(); 114 | 115 | let get = store 116 | .instance_export(module, "get") 117 | .unwrap() 118 | .as_func() 119 | .unwrap(); 120 | 121 | // Set global to 17. 3 is returned as previous (default) value. 122 | assert_eq!(3, store.invoke_typed_without_fuel(set, 17).unwrap()); 123 | 124 | // Now 17 will be returned when getting the global 125 | assert_eq!(17, store.invoke_typed_without_fuel(get, ()).unwrap()); 126 | } 127 | 128 | #[test_log::test] 129 | fn global_invalid_instr() { 130 | use wasm::validate; 131 | 132 | let wat = r#" 133 | (module 134 | (global $my_global (mut i32) 135 | i32.const 2 136 | i32.const 2 137 | i32.const 2 138 | i32.add 139 | i32.const 2 140 | i32.const 2 141 | i32.const 2 142 | i32.const 3 143 | 144 | ) 145 | ) 146 | "#; 147 | let wasm_bytes = wat::parse_str(wat).unwrap(); 148 | 149 | if validate(&wasm_bytes).is_ok() { 150 | panic!("validation succeeded") 151 | } 152 | } 153 | 154 | #[test_log::test] 155 | fn embedder_interface() { 156 | use wasm::{validate, Store}; 157 | 158 | let wat = r#" 159 | (module 160 | (global (export "global_0") (mut i32) i32.const 1) 161 | (global (export "global_1") (mut i64) i64.const 3) 162 | ) 163 | "#; 164 | let wasm_bytes = wat::parse_str(wat).unwrap(); 165 | 166 | let validation_info = validate(&wasm_bytes).expect("validation failed"); 167 | let mut store = Store::new(()); 168 | let module = store 169 | .module_instantiate(&validation_info, Vec::new(), None) 170 | .unwrap() 171 | .module_addr; 172 | 173 | let global_0 = store 174 | .instance_export(module, "global_0") 175 | .unwrap() 176 | .as_global() 177 | .expect("global"); 178 | let global_1 = store 179 | .instance_export(module, "global_1") 180 | .unwrap() 181 | .as_global() 182 | .expect("global"); 183 | 184 | assert_eq!(store.global_read(global_0), Ok(StoredValue::I32(1))); 185 | assert_eq!(store.global_read(global_1), Ok(StoredValue::I64(3))); 186 | 187 | assert_eq!(store.global_write(global_0, StoredValue::I32(33)), Ok(())); 188 | 189 | assert_eq!(store.global_read(global_0), Ok(StoredValue::I32(33))); 190 | assert_eq!(store.global_read(global_1), Ok(StoredValue::I64(3))); 191 | 192 | assert_eq!( 193 | store.global_type(global_0), 194 | Ok(GlobalType { 195 | ty: ValType::NumType(NumType::I32), 196 | is_mut: true, 197 | }) 198 | ); 199 | 200 | assert_eq!( 201 | store.global_type(global_1), 202 | Ok(GlobalType { 203 | ty: ValType::NumType(NumType::I64), 204 | is_mut: true, 205 | }) 206 | ); 207 | } 208 | -------------------------------------------------------------------------------- /tests/table_size.rs: -------------------------------------------------------------------------------- 1 | /* 2 | # This file incorporates code from the WebAssembly testsuite, originally 3 | # available at https://github.com/WebAssembly/testsuite. 4 | # 5 | # The original code is licensed under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | */ 17 | 18 | use wasm::{validate, Store}; 19 | 20 | #[test_log::test] 21 | fn table_size_test() { 22 | let w = r#" 23 | (module 24 | (table $t0 0 externref) 25 | (table $t1 1 externref) 26 | (table $t2 0 2 externref) 27 | (table $t3 3 8 externref) 28 | 29 | (func (export "size-t0") (result i32) table.size) 30 | (func (export "size-t1") (result i32) (table.size $t1)) 31 | (func (export "size-t2") (result i32) (table.size $t2)) 32 | (func (export "size-t3") (result i32) (table.size $t3)) 33 | 34 | (func (export "grow-t0") (param $sz i32) 35 | (drop (table.grow $t0 (ref.null extern) (local.get $sz))) 36 | ) 37 | (func (export "grow-t1") (param $sz i32) 38 | (drop (table.grow $t1 (ref.null extern) (local.get $sz))) 39 | ) 40 | (func (export "grow-t2") (param $sz i32) 41 | (drop (table.grow $t2 (ref.null extern) (local.get $sz))) 42 | ) 43 | (func (export "grow-t3") (param $sz i32) 44 | (drop (table.grow $t3 (ref.null extern) (local.get $sz))) 45 | ) 46 | ) 47 | "#; 48 | let wasm_bytes = wat::parse_str(w).unwrap(); 49 | let validation_info = validate(&wasm_bytes).unwrap(); 50 | let mut store = Store::new(()); 51 | let module = store 52 | .module_instantiate(&validation_info, Vec::new(), None) 53 | .unwrap() 54 | .module_addr; 55 | 56 | // let get_funcref = store.instance_export(module, "get-funcref").unwrap().as_func().unwrap(); 57 | // let init = store.instance_export(module, "init").unwrap().as_func().unwrap(); 58 | let size_t0 = store 59 | .instance_export(module, "size-t0") 60 | .unwrap() 61 | .as_func() 62 | .unwrap(); 63 | let size_t1 = store 64 | .instance_export(module, "size-t1") 65 | .unwrap() 66 | .as_func() 67 | .unwrap(); 68 | let size_t2 = store 69 | .instance_export(module, "size-t2") 70 | .unwrap() 71 | .as_func() 72 | .unwrap(); 73 | let size_t3 = store 74 | .instance_export(module, "size-t3") 75 | .unwrap() 76 | .as_func() 77 | .unwrap(); 78 | let grow_t0 = store 79 | .instance_export(module, "grow-t0") 80 | .unwrap() 81 | .as_func() 82 | .unwrap(); 83 | let grow_t1 = store 84 | .instance_export(module, "grow-t1") 85 | .unwrap() 86 | .as_func() 87 | .unwrap(); 88 | let grow_t2 = store 89 | .instance_export(module, "grow-t2") 90 | .unwrap() 91 | .as_func() 92 | .unwrap(); 93 | let grow_t3 = store 94 | .instance_export(module, "grow-t3") 95 | .unwrap() 96 | .as_func() 97 | .unwrap(); 98 | 99 | assert_eq!(store.invoke_typed_without_fuel(size_t0, ()), Ok(0)); 100 | assert_eq!(store.invoke_typed_without_fuel(size_t0, ()), Ok(0)); 101 | assert_eq!(store.invoke_typed_without_fuel(grow_t0, 1), Ok(())); 102 | assert_eq!(store.invoke_typed_without_fuel(size_t0, ()), Ok(1)); 103 | assert_eq!(store.invoke_typed_without_fuel(grow_t0, 4), Ok(())); 104 | assert_eq!(store.invoke_typed_without_fuel(size_t0, ()), Ok(5)); 105 | assert_eq!(store.invoke_typed_without_fuel(grow_t0, 0), Ok(())); 106 | assert_eq!(store.invoke_typed_without_fuel(size_t0, ()), Ok(5)); 107 | 108 | assert_eq!(store.invoke_typed_without_fuel(size_t1, ()), Ok(1)); 109 | assert_eq!(store.invoke_typed_without_fuel(grow_t1, 1), Ok(())); 110 | assert_eq!(store.invoke_typed_without_fuel(size_t1, ()), Ok(2)); 111 | assert_eq!(store.invoke_typed_without_fuel(grow_t1, 4), Ok(())); 112 | assert_eq!(store.invoke_typed_without_fuel(size_t1, ()), Ok(6)); 113 | assert_eq!(store.invoke_typed_without_fuel(grow_t1, 0), Ok(())); 114 | assert_eq!(store.invoke_typed_without_fuel(size_t1, ()), Ok(6)); 115 | 116 | assert_eq!(store.invoke_typed_without_fuel(size_t2, ()), Ok(0)); 117 | assert_eq!(store.invoke_typed_without_fuel(grow_t2, 3), Ok(())); 118 | assert_eq!(store.invoke_typed_without_fuel(size_t2, ()), Ok(0)); 119 | assert_eq!(store.invoke_typed_without_fuel(grow_t2, 1), Ok(())); 120 | assert_eq!(store.invoke_typed_without_fuel(size_t2, ()), Ok(1)); 121 | assert_eq!(store.invoke_typed_without_fuel(grow_t2, 0), Ok(())); 122 | assert_eq!(store.invoke_typed_without_fuel(size_t2, ()), Ok(1)); 123 | assert_eq!(store.invoke_typed_without_fuel(grow_t2, 4), Ok(())); 124 | assert_eq!(store.invoke_typed_without_fuel(size_t2, ()), Ok(1)); 125 | assert_eq!(store.invoke_typed_without_fuel(grow_t2, 1), Ok(())); 126 | assert_eq!(store.invoke_typed_without_fuel(size_t2, ()), Ok(2)); 127 | 128 | assert_eq!(store.invoke_typed_without_fuel(size_t3, ()), Ok(3)); 129 | assert_eq!(store.invoke_typed_without_fuel(grow_t3, 1), Ok(())); 130 | assert_eq!(store.invoke_typed_without_fuel(size_t3, ()), Ok(4)); 131 | assert_eq!(store.invoke_typed_without_fuel(grow_t3, 3), Ok(())); 132 | assert_eq!(store.invoke_typed_without_fuel(size_t3, ()), Ok(7)); 133 | assert_eq!(store.invoke_typed_without_fuel(grow_t3, 0), Ok(())); 134 | assert_eq!(store.invoke_typed_without_fuel(size_t3, ()), Ok(7)); 135 | assert_eq!(store.invoke_typed_without_fuel(grow_t3, 2), Ok(())); 136 | assert_eq!(store.invoke_typed_without_fuel(size_t3, ()), Ok(7)); 137 | assert_eq!(store.invoke_typed_without_fuel(grow_t3, 1), Ok(())); 138 | assert_eq!(store.invoke_typed_without_fuel(size_t3, ()), Ok(8)); 139 | } 140 | 141 | // ;; Type errors 142 | 143 | // (assert_invalid 144 | // (module 145 | // (table $t 1 externref) 146 | // (func $type-result-i32-vs-empty 147 | // (table.size $t) 148 | // ) 149 | // ) 150 | // "type mismatch" 151 | // ) 152 | // (assert_invalid 153 | // (module 154 | // (table $t 1 externref) 155 | // (func $type-result-i32-vs-f32 (result f32) 156 | // (table.size $t) 157 | // ) 158 | // ) 159 | // "type mismatch" 160 | // ) 161 | -------------------------------------------------------------------------------- /src/core/rw_spinlock.rs: -------------------------------------------------------------------------------- 1 | //! Naive implementation of spin based locking mechanisms 2 | //! 3 | //! This module provides implementations for locking mechanisms required. 4 | //! 5 | //! # Acknowledgement 6 | //! 7 | //! This implementation is largely inspired by the book 8 | //! ["Rust Atomics and Locks" by Mara Bos](https://marabos.nl/atomics/). 9 | 10 | use core::cell::UnsafeCell; 11 | use core::hint::{self}; 12 | use core::ops::{Deref, DerefMut}; 13 | use core::sync::atomic::{AtomicU32, Ordering}; 14 | 15 | /// A spinlock based, read-write lock which favours writers over readers 16 | /// 17 | /// # Properties 18 | /// 19 | /// - Read-write semantics allow for multiple readers at the same time, but require exclusive access 20 | /// for a writer 21 | /// - Spin based, e.g. waiting for the lock wastes CPU cycles in a busy loop 22 | /// - This design however enables an implementation independent of operating system features like 23 | /// condition variables, the only requirement are 32 bit atomics in the ISA 24 | /// - Biased towards writes: once a writer waits for the lock, all new reader wait until that writer 25 | /// got access 26 | pub struct RwSpinLock { 27 | /// The inner data protected by this lock 28 | inner: UnsafeCell, 29 | 30 | /// Lock state (on ambiguity, the state closer to the top takes precedence) 31 | /// 32 | /// - `0` means there are no readers nor any writer 33 | /// - `u32::MAX` means there is a single active writer 34 | /// - `state % 2 == 0` means there are `state / 2` active readers 35 | /// - `state % 2 != 0` means there are `(state - 1) / 2` active readers and at least one waiting 36 | /// writer 37 | state: AtomicU32, 38 | } 39 | 40 | impl RwSpinLock { 41 | /// Create a new instance of self, wrapping the `value` of type `T` 42 | pub fn new(value: T) -> Self { 43 | Self { 44 | inner: UnsafeCell::new(value), 45 | state: AtomicU32::new(0), 46 | } 47 | } 48 | 49 | // Get read access to the value wrapped in this [`RwSpinLock`] 50 | pub fn read(&self) -> ReadLockGuard<'_, T> { 51 | // get the current state 52 | let mut s = self.state.load(Ordering::Relaxed); // ordering by the book 53 | 54 | loop { 55 | // s is even, so there are maybe active readers but no active or waiting writer 56 | // -> reader can acquire read guard (as long as an overflow is avoided) 57 | if s % 2 == 0 && s < u32::MAX - 2 { 58 | match self.state.compare_exchange_weak( 59 | s, 60 | s + 2, 61 | Ordering::Acquire, 62 | Ordering::Relaxed, 63 | ) { 64 | Ok(_) => return ReadLockGuard { lock: self }, 65 | Err(update_s) => s = update_s, 66 | } 67 | } 68 | 69 | // there is one active (`s == u32::MAX`) or at least one waiting (otherwise) writer 70 | // -> spin, re-load s and try again 71 | if s % 2 == 1 { 72 | hint::spin_loop(); 73 | s = self.state.load(Ordering::Relaxed); // ordering by the book 74 | } 75 | } 76 | } 77 | 78 | // Get write access to the value wrapped in this [`RwSpinLock`] 79 | pub fn write(&self) -> WriteLockGuard<'_, T> { 80 | let mut s = self.state.load(Ordering::Relaxed); 81 | 82 | loop { 83 | // there is no active reader (`s >= 2 && s % 2 == 0`) or writer (`s == u32::MAX`) 84 | if s <= 1 { 85 | match self 86 | .state 87 | .compare_exchange(s, u32::MAX, Ordering::Acquire, Ordering::Relaxed) 88 | { 89 | Ok(_) => return WriteLockGuard { lock: self }, 90 | Err(updated_s) => { 91 | s = updated_s; 92 | continue; 93 | } 94 | } 95 | } 96 | 97 | // announce that a writer is waiting if this is not yet announced 98 | if s % 2 == 0 { 99 | match self 100 | .state 101 | // ordering by the book 102 | .compare_exchange(s, s + 1, Ordering::Relaxed, Ordering::Relaxed) 103 | { 104 | Ok(_) => {} 105 | Err(updated_s) => { 106 | s = updated_s; 107 | continue; 108 | } 109 | } 110 | } 111 | 112 | // wait was announced, there are still active readers 113 | // -> spin, re-load s, continue from the start of the lop 114 | hint::spin_loop(); 115 | s = self.state.load(Ordering::Relaxed); 116 | } 117 | } 118 | } 119 | 120 | // SAFETY: When the inner `T` is `Sync`, the `RwSpinlock` can be `Sync` as well 121 | unsafe impl Sync for RwSpinLock where T: Send + Sync {} 122 | 123 | impl Default for RwSpinLock { 124 | fn default() -> Self { 125 | Self::new(T::default()) 126 | } 127 | } 128 | 129 | /// Read guard for the [`RwSpinLock`] 130 | pub struct ReadLockGuard<'a, T> { 131 | lock: &'a RwSpinLock, 132 | } 133 | 134 | impl Deref for ReadLockGuard<'_, T> { 135 | type Target = T; 136 | fn deref(&self) -> &T { 137 | // SAFETY: For as long as a `ReadLockGuard` exists, it can dereference to a shared reference 138 | // to the inner data can be handed out 139 | unsafe { &*self.lock.inner.get() } 140 | } 141 | } 142 | 143 | impl Drop for ReadLockGuard<'_, T> { 144 | fn drop(&mut self) { 145 | self.lock.state.fetch_sub(2, Ordering::Release); // ordering by the book 146 | } 147 | } 148 | 149 | /// Write guard for the [`RwSpinLock`] 150 | pub struct WriteLockGuard<'a, T> { 151 | lock: &'a RwSpinLock, 152 | } 153 | 154 | impl Deref for WriteLockGuard<'_, T> { 155 | type Target = T; 156 | fn deref(&self) -> &T { 157 | // SAFETY: For as long as a `WriteLockGuard` exists, it can dereference to a shared reference 158 | // to the inner data 159 | unsafe { &*self.lock.inner.get() } 160 | } 161 | } 162 | 163 | impl DerefMut for WriteLockGuard<'_, T> { 164 | fn deref_mut(&mut self) -> &mut T { 165 | // SAFETY: For as long as a `WriteLockGuard` exists, it can dereference to a mutable 166 | // references to the inner data 167 | unsafe { &mut *self.lock.inner.get() } 168 | } 169 | } 170 | 171 | impl Drop for WriteLockGuard<'_, T> { 172 | fn drop(&mut self) { 173 | self.lock.state.store(0, Ordering::Release); // ordering by the book 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/execution/checked/value.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | addrs::FuncAddr, 3 | value::{ExternAddr, Ref, ValueTypeMismatchError, F32, F64}, 4 | RefType, Value, 5 | }; 6 | 7 | use super::{AbstractStored, Stored}; 8 | 9 | /// A stored variant of [`Value`] 10 | #[derive(Clone, Copy, Debug, PartialEq)] 11 | pub enum StoredValue { 12 | I32(u32), 13 | I64(u64), 14 | F32(F32), 15 | F64(F64), 16 | V128([u8; 16]), 17 | Ref(StoredRef), 18 | } 19 | 20 | impl AbstractStored for StoredValue { 21 | type BareTy = Value; 22 | 23 | unsafe fn from_bare(bare_value: Self::BareTy, id: crate::StoreId) -> Self { 24 | match bare_value { 25 | Value::I32(x) => Self::I32(x), 26 | Value::I64(x) => Self::I64(x), 27 | Value::F32(x) => Self::F32(x), 28 | Value::F64(x) => Self::F64(x), 29 | Value::V128(x) => Self::V128(x), 30 | Value::Ref(r#ref) => Self::Ref(StoredRef::from_bare(r#ref, id)), 31 | } 32 | } 33 | 34 | fn id(&self) -> Option { 35 | match self { 36 | Self::Ref(r#ref) => r#ref.id(), 37 | _ => None, 38 | } 39 | } 40 | 41 | fn into_bare(self) -> Self::BareTy { 42 | match self { 43 | Self::I32(x) => Value::I32(x), 44 | Self::I64(x) => Value::I64(x), 45 | Self::F32(x) => Value::F32(x), 46 | Self::F64(x) => Value::F64(x), 47 | Self::V128(x) => Value::V128(x), 48 | Self::Ref(stored_ref) => Value::Ref(stored_ref.into_bare()), 49 | } 50 | } 51 | } 52 | 53 | /// A stored variant of [`Ref`] 54 | #[derive(Clone, Copy, Debug, PartialEq)] 55 | pub enum StoredRef { 56 | Null(RefType), 57 | Func(Stored), 58 | /// We do not wrap [`ExternAddr`]s in a [`Stored`] object because they are 59 | /// not stored in the [`Store`](crate::Store). 60 | Extern(ExternAddr), 61 | } 62 | 63 | impl AbstractStored for StoredRef { 64 | type BareTy = Ref; 65 | 66 | unsafe fn from_bare(bare_value: Self::BareTy, id: crate::StoreId) -> Self { 67 | match bare_value { 68 | Ref::Null(ref_type) => Self::Null(ref_type), 69 | Ref::Func(func_addr) => Self::Func(Stored::from_bare(func_addr, id)), 70 | Ref::Extern(extern_addr) => Self::Extern(extern_addr), 71 | } 72 | } 73 | 74 | fn id(&self) -> Option { 75 | match self { 76 | StoredRef::Func(stored_func_addr) => stored_func_addr.id(), 77 | StoredRef::Null(_) | StoredRef::Extern(_) => None, 78 | } 79 | } 80 | 81 | fn into_bare(self) -> Self::BareTy { 82 | match self { 83 | Self::Null(ref_type) => Ref::Null(ref_type), 84 | Self::Func(stored_func_addr) => Ref::Func(stored_func_addr.into_bare()), 85 | Self::Extern(extern_addr) => Ref::Extern(extern_addr), 86 | } 87 | } 88 | } 89 | 90 | impl From for StoredValue { 91 | fn from(value: u32) -> Self { 92 | StoredValue::I32(value) 93 | } 94 | } 95 | impl TryFrom for u32 { 96 | type Error = ValueTypeMismatchError; 97 | 98 | fn try_from(value: StoredValue) -> Result { 99 | match value { 100 | StoredValue::I32(value) => Ok(value), 101 | _ => Err(ValueTypeMismatchError), 102 | } 103 | } 104 | } 105 | 106 | impl From for StoredValue { 107 | fn from(value: i32) -> Self { 108 | StoredValue::I32(u32::from_le_bytes(value.to_le_bytes())) 109 | } 110 | } 111 | impl TryFrom for i32 { 112 | type Error = ValueTypeMismatchError; 113 | 114 | fn try_from(value: StoredValue) -> Result { 115 | match value { 116 | StoredValue::I32(value) => Ok(i32::from_le_bytes(value.to_le_bytes())), 117 | _ => Err(ValueTypeMismatchError), 118 | } 119 | } 120 | } 121 | 122 | impl From for StoredValue { 123 | fn from(value: u64) -> Self { 124 | StoredValue::I64(value) 125 | } 126 | } 127 | impl TryFrom for u64 { 128 | type Error = ValueTypeMismatchError; 129 | 130 | fn try_from(value: StoredValue) -> Result { 131 | match value { 132 | StoredValue::I64(value) => Ok(value), 133 | _ => Err(ValueTypeMismatchError), 134 | } 135 | } 136 | } 137 | 138 | impl From for StoredValue { 139 | fn from(value: i64) -> Self { 140 | StoredValue::I64(u64::from_le_bytes(value.to_le_bytes())) 141 | } 142 | } 143 | impl TryFrom for i64 { 144 | type Error = ValueTypeMismatchError; 145 | 146 | fn try_from(value: StoredValue) -> Result { 147 | match value { 148 | StoredValue::I64(value) => Ok(i64::from_le_bytes(value.to_le_bytes())), 149 | _ => Err(ValueTypeMismatchError), 150 | } 151 | } 152 | } 153 | 154 | impl From for StoredValue { 155 | fn from(value: F32) -> Self { 156 | StoredValue::F32(value) 157 | } 158 | } 159 | impl TryFrom for F32 { 160 | type Error = ValueTypeMismatchError; 161 | 162 | fn try_from(value: StoredValue) -> Result { 163 | match value { 164 | StoredValue::F32(value) => Ok(value), 165 | _ => Err(ValueTypeMismatchError), 166 | } 167 | } 168 | } 169 | 170 | impl From for StoredValue { 171 | fn from(value: F64) -> Self { 172 | StoredValue::F64(value) 173 | } 174 | } 175 | impl TryFrom for F64 { 176 | type Error = ValueTypeMismatchError; 177 | 178 | fn try_from(value: StoredValue) -> Result { 179 | match value { 180 | StoredValue::F64(value) => Ok(value), 181 | _ => Err(ValueTypeMismatchError), 182 | } 183 | } 184 | } 185 | 186 | impl From<[u8; 16]> for StoredValue { 187 | fn from(value: [u8; 16]) -> Self { 188 | StoredValue::V128(value) 189 | } 190 | } 191 | impl TryFrom for [u8; 16] { 192 | type Error = ValueTypeMismatchError; 193 | 194 | fn try_from(value: StoredValue) -> Result { 195 | match value { 196 | StoredValue::V128(value) => Ok(value), 197 | _ => Err(ValueTypeMismatchError), 198 | } 199 | } 200 | } 201 | 202 | impl From for StoredValue { 203 | fn from(value: StoredRef) -> Self { 204 | StoredValue::Ref(value) 205 | } 206 | } 207 | impl TryFrom for StoredRef { 208 | type Error = ValueTypeMismatchError; 209 | 210 | fn try_from(value: StoredValue) -> Result { 211 | match value { 212 | StoredValue::Ref(value) => Ok(value), 213 | _ => Err(ValueTypeMismatchError), 214 | } 215 | } 216 | } 217 | --------------------------------------------------------------------------------