summaryrefslogtreecommitdiff
path: root/crates/cli
diff options
context:
space:
mode:
Diffstat (limited to 'crates/cli')
-rw-r--r--crates/cli/Cargo.toml29
-rw-r--r--crates/cli/README.md27
-rw-r--r--crates/cli/src/args.rs37
-rw-r--r--crates/cli/src/bin.rs119
-rw-r--r--crates/cli/src/cli.rs128
-rw-r--r--crates/cli/src/cmd/compile.rs15
-rw-r--r--crates/cli/src/cmd/completion.rs12
-rw-r--r--crates/cli/src/cmd/dump.rs71
-rw-r--r--crates/cli/src/cmd/inspect.rs35
-rw-r--r--crates/cli/src/cmd/mod.rs7
-rw-r--r--crates/cli/src/cmd/run.rs47
-rw-r--r--crates/cli/src/cmd/wast.rs12
-rw-r--r--crates/cli/src/engine_flags.rs154
-rw-r--r--crates/cli/src/lib.rs34
-rw-r--r--crates/cli/src/load.rs103
-rw-r--r--crates/cli/src/output.rs85
-rw-r--r--crates/cli/src/util.rs1
-rw-r--r--crates/cli/src/value_parse.rs55
-rw-r--r--crates/cli/src/wast_runner.rs885
-rw-r--r--crates/cli/src/wat.rs10
-rw-r--r--crates/cli/tests/cli.rs137
21 files changed, 1829 insertions, 174 deletions
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index 8472afb..add21d3 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -1,30 +1,45 @@
[package]
name="tinywasm-cli"
version.workspace=true
-description="Command-line interface for TinyWasm"
+description="Minimal command-line interface for TinyWasm"
edition.workspace=true
license.workspace=true
authors.workspace=true
repository.workspace=true
+documentation="https://docs.rs/tinywasm-cli"
rust-version.workspace=true
-keywords.workspace=true
-categories=["wasm"]
+keywords=["tinywasm", "wasm", "webassembly", "cli", "runtime"]
+categories=["command-line-utilities", "wasm"]
readme="README.md"
+[lib]
+name="tinywasm_cli"
+path="src/lib.rs"
+
[[bin]]
-name="tinywasm-cli"
+name="tinywasm"
path="src/bin.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
+clap={version="4.5", features=["derive"]}
+clap_complete="4.5"
eyre.workspace=true
log.workspace=true
pretty_env_logger.workspace=true
tinywasm={version="0.9.0-alpha.0", path="../tinywasm", features=["std", "parser"]}
-argh="0.1"
+wat={workspace=true, optional=true}
wast={workspace=true, optional=true}
+owo-colors={workspace=true}
+anstream={version="1.0"}
[features]
-default=["wat"]
-wat=["dep:wast"]
+default=["wat", "wast"]
+wat=["dep:wat"]
+wast=["dep:wast"]
+
+[dev-dependencies]
+assert_cmd="2.0"
+predicates="3.1"
+tempfile="3.20"
diff --git a/crates/cli/README.md b/crates/cli/README.md
index aa4d0c8..88172c9 100644
--- a/crates/cli/README.md
+++ b/crates/cli/README.md
@@ -1,13 +1,30 @@
# `tinywasm-cli`
-The `tinywasm-cli` crate contains the command line interface for the `tinywasm` project. See [`tinywasm`](https://crates.io/crates/tinywasm) for more information.
+The `tinywasm-cli` package installs the `tinywasm` binary for `tinywasm`. See [`tinywasm`](https://crates.io/crates/tinywasm) for the embedding API.
It is recommended to use the library directly instead of the CLI.
+The crate also exposes reusable helpers such as `tinywasm_cli::wast_runner::WastRunner` so workspace tests can drive the same WAST execution logic directly.
+
## Usage
```bash
-$ cargo install tinywasm-cli
-$ tinywasm-cli --help
-$ tinywasm-cli run ./module.wasm
-$ tinywasm-cli run ./module.wasm -f add -a i32:1 -a i32:2
+$ cargo install tinywasm-cli --version 0.9.0-alpha.0 --bin tinywasm
+$ tinywasm --help
+$ tinywasm ./module.wasm
+$ tinywasm run --invoke add ./module.wasm 1 2
+$ tinywasm compile ./module.wat -o ./module.twasm
+$ tinywasm dump ./module.twasm
+$ tinywasm inspect ./module.wasm
+$ tinywasm wast ./spec-tests/address.wast
```
+
+Notes:
+
+- `run`, `dump`, and `inspect` accept `.wasm`, `.wat`, and `.twasm` inputs.
+- Use `-` as the input path to read a module from stdin.
+- Without `--invoke`, `tinywasm` expects the module to have a start function or `_start` export.
+- `compile` writes TinyWasm's `twasm` archive format.
+- Function invocation arguments are parsed from the export signature, so `tinywasm run --invoke add ./module.wasm 1 2` works without repeating Wasm types on the command line.
+- `inspect` uses ANSI colors automatically when writing to a terminal; set `NO_COLOR=1` to disable them.
+- Stack flags support both fixed sizes like `--value-stack-size 4096` and dynamic sizes like `--value-stack-dynamic 1024:8192`.
+- `wast` is a separate command for WebAssembly spec scripts and accepts files or folders containing `.wast` files.
diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs
deleted file mode 100644
index 0333c92..0000000
--- a/crates/cli/src/args.rs
+++ /dev/null
@@ -1,37 +0,0 @@
-use std::str::FromStr;
-use tinywasm::types::WasmValue;
-
-#[derive(Debug)]
-pub struct WasmArg(WasmValue);
-
-pub fn to_wasm_args(args: Vec<WasmArg>) -> Vec<WasmValue> {
- args.into_iter().map(Into::into).collect()
-}
-
-impl From<WasmArg> for WasmValue {
- fn from(value: WasmArg) -> Self {
- value.0
- }
-}
-
-impl FromStr for WasmArg {
- type Err = String;
- fn from_str(s: &str) -> std::prelude::v1::Result<Self, Self::Err> {
- let [ty, val]: [&str; 2] = s
- .split(':')
- .collect::<Vec<_>>()
- .try_into()
- .map_err(|_e| "invalid argument format; expected type:value".to_string())?;
-
- let arg: WasmValue = match ty {
- "i32" => val.parse::<i32>().map_err(|e| format!("invalid argument value for i32: {e:?}"))?.into(),
- "i64" => val.parse::<i64>().map_err(|e| format!("invalid argument value for i64: {e:?}"))?.into(),
- "f32" => val.parse::<f32>().map_err(|e| format!("invalid argument value for f32: {e:?}"))?.into(),
- "f64" => val.parse::<f64>().map_err(|e| format!("invalid argument value for f64: {e:?}"))?.into(),
- "v128" => val.parse::<i128>().map_err(|e| format!("invalid argument value for v128: {e:?}"))?.into(),
- t => return Err(format!("invalid arg type `{t}`; expected one of i32, i64, f32, f64, v128")),
- };
-
- Ok(WasmArg(arg))
- }
-}
diff --git a/crates/cli/src/bin.rs b/crates/cli/src/bin.rs
index 7e32fed..592fda7 100644
--- a/crates/cli/src/bin.rs
+++ b/crates/cli/src/bin.rs
@@ -1,118 +1,9 @@
-use std::str::FromStr;
-
-use argh::FromArgs;
-use args::WasmArg;
+use clap::Parser;
use eyre::Result;
-use log::{debug, info};
-use tinywasm::{Module, ModuleInstance, types::WasmValue};
-
-use crate::args::to_wasm_args;
-mod args;
-mod util;
-
-#[cfg(feature = "wat")]
-mod wat;
-
-#[derive(FromArgs)]
-/// `TinyWasm` CLI
-struct TinyWasmCli {
- #[argh(subcommand)]
- nested: TinyWasmSubcommand,
-
- /// log level: trace, debug, info, warn, or error
- #[argh(option, short = 'l', default = "\"info\".to_string()")]
- log_level: String,
-}
-
-#[derive(FromArgs)]
-#[argh(subcommand)]
-enum TinyWasmSubcommand {
- Run(Run),
-}
-
-enum Engine {
- Main,
-}
-
-impl FromStr for Engine {
- type Err = String;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s {
- "main" => Ok(Self::Main),
- _ => Err(format!("unknown engine: {s}")),
- }
- }
-}
-
-#[derive(FromArgs)]
-/// run a wasm file
-#[argh(subcommand, name = "run")]
-struct Run {
- /// wasm file to run
- #[argh(positional)]
- wasm_file: String,
-
- /// exported function to run; omit to only instantiate and run start
- #[argh(option, short = 'f')]
- func: Option<String>,
-
- /// arguments passed to the function in type:value form
- #[argh(option, short = 'a')]
- args: Vec<WasmArg>,
-
- /// engine to use (currently only `main`)
- #[argh(option, short = 'e', default = "Engine::Main")]
- engine: Engine,
-}
+use tinywasm_cli::{Cli, run_cli};
fn main() -> Result<()> {
- let args: TinyWasmCli = argh::from_env();
- let level = match args.log_level.as_str() {
- "trace" => log::LevelFilter::Trace,
- "debug" => log::LevelFilter::Debug,
- "warn" => log::LevelFilter::Warn,
- "error" => log::LevelFilter::Error,
- "info" => log::LevelFilter::Info,
- other => return Err(eyre::eyre!("invalid log level `{other}`; expected trace, debug, info, warn, or error")),
- };
-
- pretty_env_logger::formatted_builder().filter_level(level).init();
- let cwd = std::env::current_dir()?;
-
- match args.nested {
- TinyWasmSubcommand::Run(Run { wasm_file, engine, args, func }) => {
- debug!("args: {args:?}");
-
- let path = cwd.join(&wasm_file);
- let module = match wasm_file.ends_with(".wat") {
- #[cfg(feature = "wat")]
- true => {
- let wat = std::fs::read_to_string(path)?;
- let wasm = wat::wat2wasm(&wat);
- tinywasm::parse_bytes(&wasm)?
- }
- #[cfg(not(feature = "wat"))]
- true => return Err(eyre::eyre!("wat support is not enabled in this build")),
- false => tinywasm::parse_file(path)?,
- };
-
- match engine {
- Engine::Main => run(module, func, &to_wasm_args(args)),
- }
- }
- }
-}
-
-fn run(module: Module, func: Option<String>, args: &[WasmValue]) -> Result<()> {
- let mut store = tinywasm::Store::default();
- let instance = ModuleInstance::instantiate(&mut store, &module, None)?;
-
- if let Some(func) = func {
- let func = instance.func_untyped(&store, &func)?;
- let res = func.call(&mut store, args)?;
- info!("{res:?}");
- }
-
- Ok(())
+ let cli = Cli::parse();
+ pretty_env_logger::formatted_builder().filter_level(cli.log_level.into()).init();
+ run_cli(cli)
}
diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs
new file mode 100644
index 0000000..8b9df2d
--- /dev/null
+++ b/crates/cli/src/cli.rs
@@ -0,0 +1,128 @@
+use clap::{
+ Args, Parser, Subcommand, ValueEnum,
+ builder::{
+ Styles,
+ styling::{AnsiColor, Effects},
+ },
+};
+use clap_complete::Shell;
+
+use crate::engine_flags::EngineFlags;
+
+// based on https://github.com/crate-ci/clap-cargo/blob/master/src/style.rs
+const STYLES: Styles = Styles::styled()
+ .header(AnsiColor::BrightGreen.on_default().effects(Effects::BOLD))
+ .usage(AnsiColor::BrightGreen.on_default().effects(Effects::BOLD))
+ .literal(AnsiColor::BrightCyan.on_default().effects(Effects::BOLD))
+ .placeholder(AnsiColor::Cyan.on_default())
+ .error(AnsiColor::BrightRed.on_default().effects(Effects::BOLD))
+ .valid(AnsiColor::BrightCyan.on_default().effects(Effects::BOLD))
+ .invalid(AnsiColor::Yellow.on_default());
+
+#[derive(Parser)]
+#[command(
+ name = "tinywasm",
+ about = "TinyWasm CLI",
+ styles = STYLES,
+ version,
+ args_conflicts_with_subcommands = true,
+ subcommand_negates_reqs = true
+)]
+pub struct Cli {
+ #[arg(long, global = true, value_enum, default_value_t = LogLevel::Info)]
+ pub log_level: LogLevel,
+
+ #[command(subcommand)]
+ pub command: Option<Commands>,
+
+ #[command(flatten)]
+ pub run: RunArgs,
+}
+
+#[derive(Subcommand)]
+pub enum Commands {
+ /// Run a module
+ Run(RunArgs),
+ /// Compile a Wasm/WAT module to a .twasm archive
+ Compile(CompileArgs),
+ /// Dump lowered TinyWasm bytecode
+ Dump(ModuleInputArgs),
+ /// Inspect imports and exports
+ Inspect(ModuleInputArgs),
+ #[cfg(feature = "wast")]
+ /// Execute WebAssembly spec scripts (.wast)
+ Wast(WastArgs),
+ /// Generate shell completions
+ Completion(CompletionArgs),
+}
+
+#[derive(Args, Clone)]
+pub struct RunArgs {
+ /// Module path, or `-` to read from stdin
+ pub module: Option<String>,
+
+ /// Invoke a named export instead of the default entrypoint
+ #[arg(long)]
+ pub invoke: Option<String>,
+
+ #[command(flatten)]
+ pub engine: EngineFlags,
+
+ /// Arguments passed to the invoked Wasm function
+ #[arg(trailing_var_arg = true)]
+ pub args: Vec<String>,
+}
+
+#[derive(Args, Clone)]
+pub struct CompileArgs {
+ /// Input module path, or `-` to read from stdin
+ pub input: String,
+
+ /// Output path, or `-` to write to stdout
+ #[arg(short, long)]
+ pub output: Option<String>,
+
+ /// Overwrite the output file if it already exists
+ #[arg(short, long)]
+ pub force: bool,
+}
+
+#[derive(Args, Clone)]
+pub struct ModuleInputArgs {
+ /// Module path, or `-` to read from stdin
+ pub module: String,
+}
+
+#[derive(Args, Clone)]
+pub struct CompletionArgs {
+ pub shell: Shell,
+}
+
+#[cfg(feature = "wast")]
+#[derive(Args, Clone)]
+pub struct WastArgs {
+ /// WAST files or directories containing .wast files
+ #[arg(required = true)]
+ pub paths: Vec<String>,
+}
+
+#[derive(Clone, Copy, ValueEnum)]
+pub enum LogLevel {
+ Trace,
+ Debug,
+ Info,
+ Warn,
+ Error,
+}
+
+impl From<LogLevel> for log::LevelFilter {
+ fn from(value: LogLevel) -> Self {
+ match value {
+ LogLevel::Trace => log::LevelFilter::Trace,
+ LogLevel::Debug => log::LevelFilter::Debug,
+ LogLevel::Info => log::LevelFilter::Info,
+ LogLevel::Warn => log::LevelFilter::Warn,
+ LogLevel::Error => log::LevelFilter::Error,
+ }
+ }
+}
diff --git a/crates/cli/src/cmd/compile.rs b/crates/cli/src/cmd/compile.rs
new file mode 100644
index 0000000..93e38d5
--- /dev/null
+++ b/crates/cli/src/cmd/compile.rs
@@ -0,0 +1,15 @@
+use eyre::Result;
+
+use crate::cli::CompileArgs;
+use crate::load::{default_twasm_output_path, load_compilable_module, write_output_bytes};
+
+pub fn run(args: CompileArgs) -> Result<()> {
+ let module = load_compilable_module(&args.input)?;
+ let twasm = module.serialize_twasm()?;
+ let output = match args.output {
+ Some(output) => output,
+ None => default_twasm_output_path(&args.input)?,
+ };
+
+ write_output_bytes(&output, &twasm, args.force)
+}
diff --git a/crates/cli/src/cmd/completion.rs b/crates/cli/src/cmd/completion.rs
new file mode 100644
index 0000000..5815f57
--- /dev/null
+++ b/crates/cli/src/cmd/completion.rs
@@ -0,0 +1,12 @@
+use std::io;
+
+use clap::CommandFactory;
+use eyre::Result;
+
+use crate::cli::{Cli, CompletionArgs};
+
+pub fn run(args: CompletionArgs) -> Result<()> {
+ let mut cmd = Cli::command();
+ clap_complete::generate(args.shell, &mut cmd, "tinywasm", &mut io::stdout());
+ Ok(())
+}
diff --git a/crates/cli/src/cmd/dump.rs b/crates/cli/src/cmd/dump.rs
new file mode 100644
index 0000000..78fb28b
--- /dev/null
+++ b/crates/cli/src/cmd/dump.rs
@@ -0,0 +1,71 @@
+use anstream::println;
+use eyre::Result;
+use owo_colors::OwoColorize;
+use tinywasm::types::{ExternalKind, ImportKind};
+
+use crate::cli::ModuleInputArgs;
+use crate::load::load_module;
+
+pub fn run(args: ModuleInputArgs) -> Result<()> {
+ let loaded = load_module(&args.module)?;
+ let module = loaded.module;
+
+ let imported_func_count =
+ module.imports.iter().filter(|import| matches!(import.kind, ImportKind::Function(_))).count() as u32;
+
+ for (func_idx, func) in module.funcs.iter().enumerate() {
+ let global_idx = imported_func_count + func_idx as u32;
+
+ let exports = module
+ .exports
+ .iter()
+ .filter(|export| export.kind == ExternalKind::Func && export.index == global_idx)
+ .map(|export| export.name.as_ref())
+ .collect::<Vec<_>>();
+
+ let header = format!("func[{func_idx}]").blue().bold().to_string();
+ if exports.is_empty() {
+ println!("{header}");
+ } else {
+ println!("{header} {}", format!("exports={}", format!("{exports:?}").cyan()).bright_black());
+ }
+
+ for (ip, instr) in func.instructions.iter().enumerate() {
+ let instr = print_instr(instr);
+ println!(" {}: {}", print_ip(ip), instr);
+ }
+ println!();
+ }
+
+ Ok(())
+}
+
+fn print_ip(ip: usize) -> String {
+ let s = format!("{ip:04}");
+ let first_non_zero = s.find(|c| c != '0').unwrap_or(s.len() - 1);
+
+ format!(
+ "{}{}",
+ &s[..first_non_zero].to_string().bright_black().dimmed(),
+ &s[first_non_zero..].to_string().bright_black()
+ )
+}
+
+fn print_instr(instr: &tinywasm::types::Instruction) -> String {
+ let instr = format!("{instr:?}");
+ let Some(split) = instr.find(['(', ' ', '{']) else {
+ return instr.bold().to_string();
+ };
+
+ let (name, rest) = instr.split_at(split);
+
+ let rest = rest
+ .replace('(', &"(".bright_black().to_string())
+ .replace(')', &")".bright_black().to_string())
+ .replace('{', &"{".bright_black().to_string())
+ .replace('}', &"}".bright_black().to_string())
+ .replace(',', &",".bright_black().to_string())
+ .replace(':', &":".bright_black().to_string());
+
+ format!("{}{}", name.bold(), rest)
+}
diff --git a/crates/cli/src/cmd/inspect.rs b/crates/cli/src/cmd/inspect.rs
new file mode 100644
index 0000000..ea2cec7
--- /dev/null
+++ b/crates/cli/src/cmd/inspect.rs
@@ -0,0 +1,35 @@
+use eyre::Result;
+
+use crate::cli::ModuleInputArgs;
+use crate::load::load_module;
+use crate::output::{format_export_type, format_import_type};
+use anstream::println;
+use owo_colors::OwoColorize;
+
+pub fn run(args: ModuleInputArgs) -> Result<()> {
+ let loaded = load_module(&args.module)?;
+ let module = loaded.module;
+
+ println!("{}", "Imports".bold());
+ let mut import_count = 0usize;
+ for import in module.imports() {
+ import_count += 1;
+ println!(" {}.{}: {}", import.module.blue(), import.name.cyan(), format_import_type(import.ty).yellow());
+ }
+ if import_count == 0 {
+ println!(" {}", "(none)".yellow());
+ }
+
+ println!();
+ println!("{}", "Exports".bold());
+ let mut export_count = 0usize;
+ for export in module.exports() {
+ export_count += 1;
+ println!(" {}: {}", export.name.green(), format_export_type(export.ty).yellow());
+ }
+ if export_count == 0 {
+ println!(" {}", "(none)".yellow());
+ }
+
+ Ok(())
+}
diff --git a/crates/cli/src/cmd/mod.rs b/crates/cli/src/cmd/mod.rs
new file mode 100644
index 0000000..0861af2
--- /dev/null
+++ b/crates/cli/src/cmd/mod.rs
@@ -0,0 +1,7 @@
+pub mod compile;
+pub mod completion;
+pub mod dump;
+pub mod inspect;
+pub mod run;
+#[cfg(feature = "wast")]
+pub mod wast;
diff --git a/crates/cli/src/cmd/run.rs b/crates/cli/src/cmd/run.rs
new file mode 100644
index 0000000..b2f5e03
--- /dev/null
+++ b/crates/cli/src/cmd/run.rs
@@ -0,0 +1,47 @@
+use eyre::{Result, bail};
+use tinywasm::types::ExportType;
+use tinywasm::{ModuleInstance, Store};
+
+use crate::cli::RunArgs;
+use crate::load::load_module;
+use crate::output::print_results;
+use crate::value_parse::parse_invocation_args;
+
+pub fn run(args: RunArgs) -> Result<()> {
+ let module_path = args.module.as_deref().ok_or_else(|| eyre::eyre!("missing module path"))?;
+ let loaded = load_module(module_path)?;
+ let mut store = Store::new(args.engine.build_engine()?);
+ let instance = ModuleInstance::instantiate_no_start(&mut store, &loaded.module, None)?;
+
+ match args.invoke.as_deref() {
+ Some(export) => {
+ if loaded.module.start_func.is_some() {
+ let _ = instance.start(&mut store)?;
+ }
+
+ let func_ty = loaded
+ .module
+ .exports()
+ .find_map(|item| match (item.name == export, item.ty) {
+ (true, ExportType::Func(ty)) => Some(ty),
+ _ => None,
+ })
+ .ok_or_else(|| eyre::eyre!("export is not a function: {export}"))?;
+ let func = instance.func_untyped(&store, export)?;
+ let params = parse_invocation_args(func_ty, &args.args)?;
+ let results = func.call(&mut store, &params)?;
+ print_results(&results);
+ Ok(())
+ }
+ None => {
+ if instance.start_func(&store)?.is_none() {
+ bail!(
+ "module has no start function or `_start` export; use `tinywasm inspect {module_path}` or `tinywasm run --invoke <export> {module_path}`"
+ )
+ }
+
+ let _ = instance.start(&mut store)?;
+ Ok(())
+ }
+ }
+}
diff --git a/crates/cli/src/cmd/wast.rs b/crates/cli/src/cmd/wast.rs
new file mode 100644
index 0000000..ef9c263
--- /dev/null
+++ b/crates/cli/src/cmd/wast.rs
@@ -0,0 +1,12 @@
+use std::path::PathBuf;
+
+use eyre::Result;
+
+use crate::cli::WastArgs;
+use crate::wast_runner::WastRunner;
+
+pub fn run(args: WastArgs) -> Result<()> {
+ let paths = args.paths.into_iter().map(PathBuf::from).collect::<Vec<_>>();
+ let mut runner = WastRunner::new();
+ runner.run_paths(&paths)
+}
diff --git a/crates/cli/src/engine_flags.rs b/crates/cli/src/engine_flags.rs
new file mode 100644
index 0000000..2965a3e
--- /dev/null
+++ b/crates/cli/src/engine_flags.rs
@@ -0,0 +1,154 @@
+use clap::{Args, ValueEnum};
+use eyre::{Result, bail};
+use tinywasm::{Engine, StackConfig, engine::FuelPolicy};
+
+#[derive(Args, Clone, Default)]
+pub struct EngineFlags {
+ /// Fuel accounting policy for budgeted execution APIs
+ #[arg(long, value_enum)]
+ pub fuel_policy: Option<FuelPolicyArg>,
+
+ /// Trap immediately on memory or stack allocation failure
+ #[arg(long)]
+ pub trap_on_oom: bool,
+
+ /// Memory backend to use for instantiated memories
+ #[arg(long, value_enum)]
+ pub memory_backend: Option<MemoryBackendArg>,
+
+ /// Chunk size in bytes for the paged memory backend
+ #[arg(long, default_value_t = 64 * 1024)]
+ pub memory_page_chunk_size: usize,
+
+ /// Fixed value stack size for all value lanes
+ #[arg(long, conflicts_with = "value_stack_dynamic")]
+ pub value_stack_size: Option<usize>,
+
+ /// Dynamic value stack config in initial:max form for all value lanes
+ #[arg(long, value_name = "INITIAL:MAX", conflicts_with = "value_stack_size")]
+ pub value_stack_dynamic: Option<StackSpec>,
+
+ /// Fixed call stack size in frames
+ #[arg(long, conflicts_with = "call_stack_dynamic")]
+ pub call_stack_size: Option<usize>,
+
+ /// Dynamic call stack config in initial:max form
+ #[arg(long, value_name = "INITIAL:MAX", conflicts_with = "call_stack_size")]
+ pub call_stack_dynamic: Option<StackSpec>,
+}
+
+#[derive(Clone, Copy, ValueEnum)]
+pub enum FuelPolicyArg {
+ PerInstruction,
+ Weighted,
+}
+
+#[derive(Clone, Copy, ValueEnum)]
+pub enum MemoryBackendArg {
+ Vec,
+ Paged,
+}
+
+#[derive(Clone)]
+pub struct StackSpec {
+ initial: usize,
+ max: usize,
+}
+
+impl core::str::FromStr for StackSpec {
+ type Err = String;
+
+ fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+ let (initial, max) = s.split_once(':').ok_or_else(|| "expected INITIAL:MAX".to_string())?;
+ let initial = initial.parse::<usize>().map_err(|e| format!("invalid initial stack size: {e}"))?;
+ let max = max.parse::<usize>().map_err(|e| format!("invalid max stack size: {e}"))?;
+ if initial > max {
+ return Err("initial stack size must be less than or equal to max stack size".to_string());
+ }
+ Ok(Self { initial, max })
+ }
+}
+
+impl StackSpec {
+ fn into_stack_config(self) -> StackConfig {
+ StackConfig::dynamic(self.initial, self.max)
+ }
+}
+
+impl EngineFlags {
+ pub fn build_engine(&self) -> Result<Engine> {
+ let mut config = tinywasm::engine::Config::new();
+
+ if let Some(fuel_policy) = self.fuel_policy {
+ config = config.with_fuel_policy(match fuel_policy {
+ FuelPolicyArg::PerInstruction => FuelPolicy::PerInstruction,
+ FuelPolicyArg::Weighted => FuelPolicy::Weighted,
+ });
+ }
+
+ if let Some(memory_backend) = self.memory_backend {
+ config = config.with_memory_backend(match memory_backend {
+ MemoryBackendArg::Vec => tinywasm::MemoryBackend::vec(),
+ MemoryBackendArg::Paged => {
+ if self.memory_page_chunk_size == 0 {
+ bail!("--memory-page-chunk-size must be greater than zero")
+ }
+ tinywasm::MemoryBackend::paged(self.memory_page_chunk_size)
+ }
+ });
+ }
+
+ if let Some(value_stack_size) = self.value_stack_size {
+ config = config.with_value_stack(StackConfig::fixed(value_stack_size));
+ }
+
+ if let Some(value_stack_dynamic) = self.value_stack_dynamic.clone() {
+ config = config.with_value_stack(value_stack_dynamic.into_stack_config());
+ }
+
+ if let Some(call_stack_size) = self.call_stack_size {
+ config = config.with_call_stack(StackConfig::fixed(call_stack_size));
+ }
+
+ if let Some(call_stack_dynamic) = self.call_stack_dynamic.clone() {
+ config = config.with_call_stack(call_stack_dynamic.into_stack_config());
+ }
+
+ if self.trap_on_oom {
+ config = config.with_trap_on_oom(true);
+ }
+
+ Ok(Engine::new(config))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parses_stack_spec() {
+ let spec: StackSpec = "16:64".parse().unwrap();
+ let cfg = spec.into_stack_config();
+ assert_eq!(cfg.initial_size, 16);
+ assert_eq!(cfg.max_size, 64);
+ assert!(cfg.dynamic);
+ }
+
+ #[test]
+ fn builds_dynamic_stack_engine() {
+ let flags = EngineFlags {
+ value_stack_dynamic: Some("8:32".parse().unwrap()),
+ call_stack_dynamic: Some("4:12".parse().unwrap()),
+ ..Default::default()
+ };
+
+ let engine = flags.build_engine().unwrap();
+ assert!(engine.config().value_stack_32.dynamic);
+ assert_eq!(engine.config().value_stack_32.initial_size, 8);
+ assert_eq!(engine.config().value_stack_32.max_size, 32);
+ assert!(engine.config().call_stack.dynamic);
+ assert_eq!(engine.config().call_stack.initial_size, 4);
+ assert_eq!(engine.config().call_stack.max_size, 12);
+ }
+}
diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs
new file mode 100644
index 0000000..bef6b72
--- /dev/null
+++ b/crates/cli/src/lib.rs
@@ -0,0 +1,34 @@
+pub mod cli;
+pub mod cmd;
+pub mod engine_flags;
+pub mod load;
+pub mod output;
+pub mod value_parse;
+#[cfg(feature = "wast")]
+pub mod wast_runner;
+
+use clap::CommandFactory;
+use eyre::Result;
+
+pub use cli::{Cli, Commands};
+
+pub fn run_cli(cli: Cli) -> Result<()> {
+ match cli.command {
+ Some(Commands::Run(args)) => cmd::run::run(args),
+ Some(Commands::Compile(args)) => cmd::compile::run(args),
+ Some(Commands::Dump(args)) => cmd::dump::run(args),
+ Some(Commands::Inspect(args)) => cmd::inspect::run(args),
+ #[cfg(feature = "wast")]
+ Some(Commands::Wast(args)) => cmd::wast::run(args),
+ Some(Commands::Completion(args)) => cmd::completion::run(args),
+ None => match cli.run.module.as_deref() {
+ Some(_) => cmd::run::run(cli.run),
+ None => {
+ let mut cmd = Cli::command();
+ cmd.print_help()?;
+ println!();
+ Ok(())
+ }
+ },
+ }
+}
diff --git a/crates/cli/src/load.rs b/crates/cli/src/load.rs
new file mode 100644
index 0000000..d16469b
--- /dev/null
+++ b/crates/cli/src/load.rs
@@ -0,0 +1,103 @@
+use std::ffi::OsStr;
+use std::io::{Read, Write};
+use std::path::Path;
+
+use eyre::{Context, Result, bail};
+use tinywasm::Module;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum InputFormat {
+ Wasm,
+ Wat,
+ Twasm,
+}
+
+pub struct LoadedModule {
+ pub module: Module,
+ pub format: InputFormat,
+}
+
+pub fn load_module(input: &str) -> Result<LoadedModule> {
+ let bytes = read_input_bytes(input)?;
+ load_module_from_bytes(input, &bytes)
+}
+
+pub fn load_compilable_module(input: &str) -> Result<Module> {
+ let loaded = load_module(input)?;
+ if loaded.format == InputFormat::Twasm {
+ bail!("input is already a twasm archive; use `run`, `dump`, or `inspect` instead")
+ }
+ Ok(loaded.module)
+}
+
+pub fn default_twasm_output_path(input: &str) -> Result<String> {
+ if input == "-" {
+ bail!("--output is required when compiling from stdin")
+ }
+
+ let path = Path::new(input);
+ let stem = path.file_stem().and_then(OsStr::to_str).unwrap_or("module");
+ let output = path.with_file_name(format!("{stem}.twasm"));
+ Ok(output.to_string_lossy().into_owned())
+}
+
+pub fn write_output_bytes(output: &str, bytes: &[u8], force: bool) -> Result<()> {
+ if output == "-" {
+ std::io::stdout().write_all(bytes)?;
+ std::io::stdout().flush()?;
+ return Ok(());
+ }
+
+ let path = Path::new(output);
+ if path.exists() && !force {
+ bail!("output file already exists: {output}; pass --force to overwrite")
+ }
+
+ std::fs::write(path, bytes).with_context(|| format!("failed to write output file `{output}`"))?;
+ Ok(())
+}
+
+fn load_module_from_bytes(input: &str, bytes: &[u8]) -> Result<LoadedModule> {
+ if bytes.starts_with(b"TWAS") {
+ let module = Module::try_from_twasm(bytes).with_context(|| format!("failed to read twasm input `{input}`"))?;
+ return Ok(LoadedModule { module, format: InputFormat::Twasm });
+ }
+
+ #[cfg(feature = "wat")]
+ if input != "-" && has_extension(input, "wat") {
+ let wasm = wat::parse_bytes(bytes).with_context(|| format!("failed to parse WAT input `{input}`"))?;
+ let module =
+ tinywasm::parse_bytes(&wasm).with_context(|| format!("failed to parse Wasm generated from `{input}`"))?;
+ return Ok(LoadedModule { module, format: InputFormat::Wat });
+ }
+
+ #[cfg(not(feature = "wat"))]
+ if input != "-" && has_extension(input, "wat") {
+ bail!("wat support is not enabled in this build")
+ }
+
+ #[cfg(feature = "wat")]
+ if input == "-"
+ && let Ok(wasm) = wat::parse_bytes(bytes)
+ {
+ let module = tinywasm::parse_bytes(&wasm).context("failed to parse Wasm generated from stdin WAT input")?;
+ return Ok(LoadedModule { module, format: InputFormat::Wat });
+ }
+
+ let module = tinywasm::parse_bytes(bytes).with_context(|| format!("failed to parse Wasm input `{input}`"))?;
+ Ok(LoadedModule { module, format: InputFormat::Wasm })
+}
+
+fn read_input_bytes(input: &str) -> Result<Vec<u8>> {
+ if input == "-" {
+ let mut bytes = Vec::new();
+ std::io::stdin().read_to_end(&mut bytes).context("failed to read stdin")?;
+ return Ok(bytes);
+ }
+
+ std::fs::read(input).with_context(|| format!("failed to read input `{input}`"))
+}
+
+fn has_extension(path: &str, extension: &str) -> bool {
+ Path::new(path).extension().and_then(OsStr::to_str) == Some(extension)
+}
diff --git a/crates/cli/src/output.rs b/crates/cli/src/output.rs
new file mode 100644
index 0000000..bc0e450
--- /dev/null
+++ b/crates/cli/src/output.rs
@@ -0,0 +1,85 @@
+use tinywasm::types::{
+ ExportType, FuncType, GlobalType, ImportType, MemoryArch, MemoryType, TableType, WasmType, WasmValue,
+};
+
+pub fn print_results(results: &[WasmValue]) {
+ match results {
+ [] => {}
+ [value] => println!("{}", format_value(value)),
+ values => {
+ let formatted = values.iter().map(format_value).collect::<Vec<_>>().join(", ");
+ println!("[{formatted}]");
+ }
+ }
+}
+
+pub fn format_value(value: &WasmValue) -> String {
+ format!("{value:?}")
+}
+
+pub fn color_enabled() -> bool {
+ use std::io::IsTerminal;
+
+ std::io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none()
+}
+
+pub fn format_wasm_type(ty: WasmType) -> &'static str {
+ match ty {
+ WasmType::I32 => "i32",
+ WasmType::I64 => "i64",
+ WasmType::F32 => "f32",
+ WasmType::F64 => "f64",
+ WasmType::V128 => "v128",
+ WasmType::RefFunc => "funcref",
+ WasmType::RefExtern => "externref",
+ }
+}
+
+pub fn format_func_type(ty: &FuncType) -> String {
+ let params = ty.params().iter().map(|ty| format_wasm_type(*ty)).collect::<Vec<_>>().join(", ");
+ let results = ty.results().iter().map(|ty| format_wasm_type(*ty)).collect::<Vec<_>>().join(", ");
+
+ if ty.results().is_empty() { format!("({params})") } else { format!("({params}) -> ({results})") }
+}
+
+pub fn format_memory_type(ty: &MemoryType) -> String {
+ let arch = match ty.arch() {
+ MemoryArch::I32 => "i32",
+ MemoryArch::I64 => "i64",
+ };
+ let max = if ty.page_count_max() == ty.page_count_initial() && ty.max_size() == ty.initial_size() {
+ ty.page_count_initial().to_string()
+ } else {
+ ty.page_count_max().to_string()
+ };
+
+ format!("memory[{arch}] initial={} max={} page_size={}", ty.page_count_initial(), max, ty.page_size())
+}
+
+pub fn format_table_type(ty: &TableType) -> String {
+ let max = ty.size_max.map(|v| v.to_string()).unwrap_or_else(|| "unbounded".to_string());
+ format!("table[{}] initial={} max={max}", format_wasm_type(ty.element_type), ty.size_initial)
+}
+
+pub fn format_global_type(ty: &GlobalType) -> String {
+ let mutability = if ty.mutable { "mut" } else { "const" };
+ format!("global[{mutability} {}]", format_wasm_type(ty.ty))
+}
+
+pub fn format_export_type(ty: ExportType<'_>) -> String {
+ match ty {
+ ExportType::Func(ty) => format!("func {}", format_func_type(ty)),
+ ExportType::Memory(ty) => format_memory_type(ty),
+ ExportType::Table(ty) => format_table_type(ty),
+ ExportType::Global(ty) => format_global_type(ty),
+ }
+}
+
+pub fn format_import_type(ty: ImportType<'_>) -> String {
+ match ty {
+ ImportType::Func(ty) => format!("func {}", format_func_type(ty)),
+ ImportType::Memory(ty) => format_memory_type(ty),
+ ImportType::Table(ty) => format_table_type(ty),
+ ImportType::Global(ty) => format_global_type(ty),
+ }
+}
diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs
deleted file mode 100644
index 8b13789..0000000
--- a/crates/cli/src/util.rs
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/crates/cli/src/value_parse.rs b/crates/cli/src/value_parse.rs
new file mode 100644
index 0000000..f64d335
--- /dev/null
+++ b/crates/cli/src/value_parse.rs
@@ -0,0 +1,55 @@
+use eyre::{Result, bail};
+use tinywasm::types::{FuncType, WasmType, WasmValue};
+
+use crate::output::format_wasm_type;
+
+pub fn parse_invocation_args(ty: &FuncType, args: &[String]) -> Result<Vec<WasmValue>> {
+ if args.len() != ty.params().len() {
+ bail!("wrong number of arguments: expected {}, got {}", ty.params().len(), args.len())
+ }
+
+ ty.params().iter().enumerate().map(|(idx, param_ty)| parse_arg(idx, *param_ty, &args[idx])).collect()
+}
+
+fn parse_arg(index: usize, ty: WasmType, value: &str) -> Result<WasmValue> {
+ let parsed = match ty {
+ WasmType::I32 => value.parse::<i32>().map(WasmValue::from).map_err(|e| format_error(index, ty, value, e))?,
+ WasmType::I64 => value.parse::<i64>().map(WasmValue::from).map_err(|e| format_error(index, ty, value, e))?,
+ WasmType::F32 => value.parse::<f32>().map(WasmValue::from).map_err(|e| format_error(index, ty, value, e))?,
+ WasmType::F64 => value.parse::<f64>().map(WasmValue::from).map_err(|e| format_error(index, ty, value, e))?,
+ WasmType::V128 => value.parse::<i128>().map(WasmValue::from).map_err(|e| format_error(index, ty, value, e))?,
+ WasmType::RefFunc | WasmType::RefExtern => {
+ bail!(
+ "unsupported CLI argument type at position {}: {}; use the embedding API for reference values",
+ index + 1,
+ format_wasm_type(ty)
+ )
+ }
+ };
+
+ Ok(parsed)
+}
+
+fn format_error(index: usize, ty: WasmType, value: &str, error: impl core::fmt::Display) -> eyre::Report {
+ eyre::eyre!("failed to parse argument {} as {} from `{value}`: {error}", index + 1, format_wasm_type(ty))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parses_numeric_args() {
+ let ty = FuncType::new(&[WasmType::I32, WasmType::F64], &[WasmType::I32]);
+ let args = vec!["1".to_string(), "2.5".to_string()];
+ let parsed = parse_invocation_args(&ty, &args).unwrap();
+ assert_eq!(parsed, vec![WasmValue::I32(1), WasmValue::F64(2.5)]);
+ }
+
+ #[test]
+ fn rejects_wrong_arity() {
+ let ty = FuncType::new(&[WasmType::I32], &[]);
+ let err = parse_invocation_args(&ty, &[]).unwrap_err();
+ assert!(err.to_string().contains("wrong number of arguments"));
+ }
+}
diff --git a/crates/cli/src/wast_runner.rs b/crates/cli/src/wast_runner.rs
new file mode 100644
index 0000000..8a759b7
--- /dev/null
+++ b/crates/cli/src/wast_runner.rs
@@ -0,0 +1,885 @@
+use std::collections::{BTreeMap, HashMap};
+use std::fmt::{Display, Formatter};
+use std::fs::canonicalize;
+use std::path::PathBuf;
+use std::{
+ panic::{self, AssertUnwindSafe},
+ time::Duration,
+};
+
+use eyre::{Context, Result, bail, eyre};
+use log::{debug, error};
+use tinywasm::types::{ExternRef, FuncRef, MemoryType, TableType, WasmType, WasmValue};
+use tinywasm::{ExecProgress, Global, HostFunction, Imports, Memory, Module, ModuleInstance, Store, Table};
+use wast::{QuoteWat, core::AbstractHeapType};
+
+const TEST_TIME_SLICE: Duration = Duration::from_millis(20);
+const TEST_MAX_SUSPENSIONS: u32 = 1000;
+
+#[derive(Default)]
+struct ModuleRegistry {
+ modules: HashMap<String, ModuleInstance>,
+ named_modules: HashMap<String, ModuleInstance>,
+ last_module: Option<ModuleInstance>,
+}
+
+impl ModuleRegistry {
+ fn modules(&self) -> &HashMap<String, ModuleInstance> {
+ &self.modules
+ }
+
+ fn update_last_module(&mut self, module: ModuleInstance, name: Option<String>) {
+ self.last_module = Some(module.clone());
+ if let Some(name) = name {
+ self.named_modules.insert(name, module);
+ }
+ }
+
+ fn register(&mut self, name: String, module: ModuleInstance) {
+ debug!("registering module: {name}");
+ self.modules.insert(name.clone(), module.clone());
+ self.last_module = Some(module.clone());
+ self.named_modules.insert(name, module);
+ }
+
+ fn get_idx(&self, module_id: Option<wast::token::Id<'_>>) -> Option<u32> {
+ match module_id {
+ Some(module) => self
+ .modules
+ .get(module.name())
+ .or_else(|| self.named_modules.get(module.name()))
+ .map(ModuleInstance::id),
+ None => self.last_module.as_ref().map(ModuleInstance::id),
+ }
+ }
+
+ fn get(&self, module_id: Option<wast::token::Id<'_>>) -> Option<ModuleInstance> {
+ match module_id {
+ Some(module_id) => {
+ self.modules.get(module_id.name()).or_else(|| self.named_modules.get(module_id.name())).cloned()
+ }
+ None => self.last_module.clone(),
+ }
+ }
+
+ fn last(&self) -> Option<ModuleInstance> {
+ self.last_module.clone()
+ }
+}
+
+#[derive(Default)]
+pub struct WastRunner(BTreeMap<String, TestGroup>);
+
+#[derive(Clone, Debug)]
+pub struct GroupResult {
+ pub name: String,
+ pub file: String,
+ pub passed: usize,
+ pub failed: usize,
+}
+
+impl WastRunner {
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ pub fn run_paths(&mut self, tests: &[PathBuf]) -> Result<()> {
+ for path in expand_paths(tests)? {
+ let contents =
+ std::fs::read_to_string(&path).context(format!("failed to read file: {}", path.to_string_lossy()))?;
+
+ let file = TestFile {
+ contents: &contents,
+ name: path.to_string_lossy().to_string(),
+ parent: canonicalize(&path)?.to_string_lossy().to_string(),
+ };
+
+ self.run_file(file)?;
+ }
+
+ self.print_errors();
+ if self.failed() {
+ anstream::println!("{self}");
+ Err(eyre!("failed one or more tests"))
+ } else {
+ anstream::println!("{self}");
+ Ok(())
+ }
+ }
+
+ pub fn set_log_level(level: log::LevelFilter) {
+ let _ = pretty_env_logger::formatted_builder().filter_level(level).try_init();
+ }
+
+ pub fn failed(&self) -> bool {
+ self.0.values().any(|group| group.stats().1 > 0)
+ }
+
+ pub fn print_errors(&self) {
+ for group in self.0.values() {
+ for test in &group.tests {
+ if let Err(err) = &test.result {
+ eprintln!(
+ "{}:{}:{} {} failed: {}",
+ group.file,
+ test.linecol.0 + 1,
+ test.linecol.1 + 1,
+ test.name,
+ err
+ );
+ }
+ }
+ }
+ }
+
+ pub fn group_results(&self) -> Vec<GroupResult> {
+ self.0
+ .iter()
+ .map(|(name, group)| {
+ let (passed, failed) = group.stats();
+ GroupResult { name: name.clone(), file: group.file.clone(), passed, failed }
+ })
+ .collect()
+ }
+
+ fn test_group(&mut self, name: &str, file: &str) -> &mut TestGroup {
+ self.0.entry(name.to_string()).or_insert_with(|| TestGroup::new(file))
+ }
+
+ pub fn run_files<'a>(&mut self, tests: impl IntoIterator<Item = TestFile<'a>>) -> Result<()> {
+ for file in tests {
+ self.run_file(file)?;
+ }
+ Ok(())
+ }
+
+ fn imports(store: &mut Store, modules: &HashMap<String, ModuleInstance>) -> Result<Imports> {
+ let mut imports = Imports::new();
+
+ let table = Table::new(
+ store,
+ TableType::new(WasmType::RefFunc, 10, Some(20)),
+ WasmValue::default_for(WasmType::RefFunc),
+ )?;
+ let memory = Memory::new(store, MemoryType::default().with_page_count_initial(1).with_page_count_max(Some(2)))?;
+ let global_i32 =
+ Global::new(store, tinywasm::types::GlobalType::new(WasmType::I32, false), WasmValue::I32(666))?;
+ let global_i64 =
+ Global::new(store, tinywasm::types::GlobalType::new(WasmType::I64, false), WasmValue::I64(666))?;
+ let global_f32 =
+ Global::new(store, tinywasm::types::GlobalType::new(WasmType::F32, false), WasmValue::F32(666.6))?;
+ let global_f64 =
+ Global::new(store, tinywasm::types::GlobalType::new(WasmType::F64, false), WasmValue::F64(666.6))?;
+
+ imports
+ .define("spectest", "memory", memory)
+ .define("spectest", "table", table)
+ .define("spectest", "global_i32", global_i32)
+ .define("spectest", "global_i64", global_i64)
+ .define("spectest", "global_f32", global_f32)
+ .define("spectest", "global_f64", global_f64)
+ .define("spectest", "print", HostFunction::from(store, |_ctx: tinywasm::FuncContext, (): ()| Ok(())))
+ .define("spectest", "print_i32", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: i32| Ok(())))
+ .define("spectest", "print_i64", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: i64| Ok(())))
+ .define("spectest", "print_f32", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: f32| Ok(())))
+ .define("spectest", "print_f64", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: f64| Ok(())))
+ .define(
+ "spectest",
+ "print_i32_f32",
+ HostFunction::from(store, |_ctx: tinywasm::FuncContext, _args: (i32, f32)| Ok(())),
+ )
+ .define(
+ "spectest",
+ "print_f64_f64",
+ HostFunction::from(store, |_ctx: tinywasm::FuncContext, _args: (f64, f64)| Ok(())),
+ );
+
+ for (name, module) in modules {
+ imports.link_module(name, module.clone())?;
+ }
+
+ Ok(imports)
+ }
+
+ pub fn run_file(&mut self, file: TestFile<'_>) -> Result<()> {
+ let test_group = self.test_group(file.name(), file.parent());
+ let wast_raw = file.raw();
+ let wast = file.wast()?;
+ let directives = wast.directives()?;
+
+ let mut store = Store::default();
+ let mut module_registry = ModuleRegistry::default();
+
+ println!("running {} tests for group: {}", directives.len(), file.name());
+ for (i, directive) in directives.into_iter().enumerate() {
+ let span = directive.span();
+ use wast::WastDirective::{
+ AssertExhaustion, AssertInvalid, AssertMalformed, AssertReturn, AssertTrap, AssertUnlinkable, Invoke,
+ Module as Wat, Register,
+ };
+
+ match directive {
+ Register { span, name, .. } => {
+ let Some(last) = module_registry.last() else {
+ test_group.add_result(
+ &format!("Register({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("no module to register")),
+ );
+ continue;
+ };
+ module_registry.register(name.to_string(), last);
+ test_group.add_result(&format!("Register({i})"), span.linecol_in(wast_raw), Ok(()));
+ }
+ Wat(module) => {
+ let result = catch_unwind_silent(|| {
+ let (name, bytes) = encode_quote_wat(module);
+ let module = parse_module_bytes(&bytes).expect("failed to parse module bytes");
+ let imports = Self::imports(&mut store, module_registry.modules()).unwrap();
+ let module_instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))
+ .expect("failed to instantiate module");
+ (name, module_instance)
+ })
+ .map_err(|e| eyre!("failed to parse wat module: {}", try_downcast_panic(e)));
+
+ match &result {
+ Err(err) => debug!("failed to parse module: {err:?}"),
+ Ok((name, module)) => module_registry.update_last_module(module.clone(), name.clone()),
+ };
+
+ test_group.add_result(&format!("Wat({i})"), span.linecol_in(wast_raw), result.map(|_| ()));
+ }
+ AssertMalformed { span, mut module, message } => {
+ let Ok(encoded) = module.encode() else {
+ test_group.add_result(&format!("AssertMalformed({i})"), span.linecol_in(wast_raw), Ok(()));
+ continue;
+ };
+ let res = catch_unwind_silent(|| parse_module_bytes(&encoded))
+ .map_err(|e| eyre!("failed to parse module (expected): {}", try_downcast_panic(e)))
+ .and_then(|res| res);
+ test_group.add_result(
+ &format!("AssertMalformed({i})"),
+ span.linecol_in(wast_raw),
+ match res {
+ Ok(_) => {
+ if message == "zero byte expected"
+ || message == "integer representation too long"
+ || message == "zero flag expected"
+ {
+ continue;
+ }
+ Err(eyre!("expected module to be malformed: {message}"))
+ }
+ Err(_) => Ok(()),
+ },
+ );
+ }
+ AssertInvalid { span, mut module, message } => {
+ if ["multiple memories", "type mismatch"].contains(&message) {
+ test_group.add_result(&format!("AssertInvalid({i})"), span.linecol_in(wast_raw), Ok(()));
+ continue;
+ }
+ let res = catch_unwind_silent(move || parse_module_bytes(&module.encode().unwrap()))
+ .map_err(|e| eyre!("failed to parse module (invalid): {}", try_downcast_panic(e)))
+ .and_then(|res| res);
+ test_group.add_result(
+ &format!("AssertInvalid({i})"),
+ span.linecol_in(wast_raw),
+ match res {
+ Ok(_) => Err(eyre!("expected module to be invalid")),
+ Err(_) => Ok(()),
+ },
+ );
+ }
+ AssertExhaustion { call, message, span } => {
+ let module = module_registry.get_idx(call.module);
+ let args = convert_wastargs(call.args)?;
+ let res =
+ catch_unwind_silent(|| exec_fn_instance(module, &mut store, call.name, &args).map(|_| ()));
+ let Ok(Err(tinywasm::Error::Trap(trap))) = res else {
+ test_group.add_result(
+ &format!("AssertExhaustion({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("expected trap")),
+ );
+ continue;
+ };
+ if !message.starts_with(trap.message()) && !trap.message().starts_with(message) {
+ test_group.add_result(
+ &format!("AssertExhaustion({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("expected trap: {}, got: {}", message, trap.message())),
+ );
+ continue;
+ }
+ test_group.add_result(&format!("AssertExhaustion({i})"), span.linecol_in(wast_raw), Ok(()));
+ }
+ AssertTrap { exec, message, span } => {
+ let res: Result<tinywasm::Result<()>, _> = catch_unwind_silent(|| {
+ let invoke = match exec {
+ wast::WastExecute::Wat(mut wat) => {
+ let module = parse_module_bytes(&wat.encode().expect("failed to encode module"))
+ .expect("failed to parse module");
+ let imports = Self::imports(&mut store, module_registry.modules()).unwrap();
+ ModuleInstance::instantiate(&mut store, &module, Some(imports))?;
+ return Ok(());
+ }
+ wast::WastExecute::Get { .. } => panic!("get not supported"),
+ wast::WastExecute::Invoke(invoke) => invoke,
+ };
+ let module = module_registry.get_idx(invoke.module);
+ let args =
+ convert_wastargs(invoke.args).map_err(|err| tinywasm::Error::Other(err.to_string()))?;
+ exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ())
+ });
+ match res {
+ Err(err) => test_group.add_result(
+ &format!("AssertTrap({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("test panicked: {}", try_downcast_panic(err))),
+ ),
+ Ok(Err(tinywasm::Error::Trap(trap))) => {
+ if !message.starts_with(trap.message()) && !trap.message().starts_with(message) {
+ test_group.add_result(
+ &format!("AssertTrap({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("expected trap: {}, got: {}", message, trap.message())),
+ );
+ continue;
+ }
+ test_group.add_result(&format!("AssertTrap({i})"), span.linecol_in(wast_raw), Ok(()));
+ }
+ Ok(Err(err)) => test_group.add_result(
+ &format!("AssertTrap({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("expected trap, {}, got: {:?}", message, err)),
+ ),
+ Ok(Ok(())) => test_group.add_result(
+ &format!("AssertTrap({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("expected trap {}, got Ok", message)),
+ ),
+ }
+ }
+ AssertUnlinkable { mut module, span, message } => {
+ let res = catch_unwind_silent(|| {
+ let module = parse_module_bytes(&module.encode().expect("failed to encode module"))
+ .expect("failed to parse module");
+ let imports = Self::imports(&mut store, module_registry.modules()).unwrap();
+ ModuleInstance::instantiate(&mut store, &module, Some(imports))
+ });
+ match res {
+ Err(err) => test_group.add_result(
+ &format!("AssertUnlinkable({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("test panicked: {}", try_downcast_panic(err))),
+ ),
+ Ok(Err(tinywasm::Error::Linker(err))) => {
+ if err.message() != message
+ && (err.message() == "memory types incompatible"
+ && message != "incompatible import type")
+ {
+ test_group.add_result(
+ &format!("AssertUnlinkable({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("expected linker error: {}, got: {}", message, err.message())),
+ );
+ continue;
+ }
+ test_group.add_result(&format!("AssertUnlinkable({i})"), span.linecol_in(wast_raw), Ok(()));
+ }
+ Ok(Err(err)) => test_group.add_result(
+ &format!("AssertUnlinkable({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("expected linker error, {}, got: {:?}", message, err)),
+ ),
+ Ok(Ok(_)) => test_group.add_result(
+ &format!("AssertUnlinkable({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("expected linker error {}, got Ok", message)),
+ ),
+ }
+ }
+ Invoke(invoke) => {
+ let name = invoke.name;
+ let res: Result<Result<()>, _> = catch_unwind_silent(|| {
+ let args = convert_wastargs(invoke.args)?;
+ let module = module_registry.get_idx(invoke.module);
+ exec_fn_instance(module, &mut store, invoke.name, &args).map_err(|e| {
+ error!("failed to execute function: {e:?}");
+ e
+ })?;
+ Ok(())
+ });
+ let res = res.map_err(|e| eyre!("test panicked: {}", try_downcast_panic(e))).and_then(|r| r);
+ test_group.add_result(&format!("Invoke({name}-{i})"), span.linecol_in(wast_raw), res);
+ }
+ AssertReturn { span, exec, results } => {
+ let expected_alternatives = match convert_wastret(results.into_iter()) {
+ Err(err) => {
+ test_group.add_result(
+ &format!("AssertReturn(unsupported-{i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("failed to convert expected results: {err:?}")),
+ );
+ continue;
+ }
+ Ok(expected) => expected,
+ };
+
+ let invoke = match match exec {
+ wast::WastExecute::Wat(_) => Err(eyre!("wat not supported")),
+ wast::WastExecute::Get { module: module_id, global, .. } => {
+ let Some(module) = module_registry.get(module_id) else {
+ test_group.add_result(
+ &format!("AssertReturn(unsupported-{i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("no module to get global from")),
+ );
+ continue;
+ };
+ let module_global = match module.global_get(&store, global) {
+ Ok(value) => value,
+ Err(err) => {
+ test_group.add_result(
+ &format!("AssertReturn(unsupported-{i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("failed to get global: {err:?}")),
+ );
+ continue;
+ }
+ };
+ let expected = expected_alternatives
+ .iter()
+ .filter_map(|alts| alts.first())
+ .find(|exp| module_global.eq_loose(exp));
+ if expected.is_none() {
+ test_group.add_result(
+ &format!("AssertReturn(unsupported-{i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!(
+ "global value did not match any expected alternative: {:?}",
+ module_global
+ )),
+ );
+ continue;
+ }
+ test_group.add_result(
+ &format!("AssertReturn({global}-{i})"),
+ span.linecol_in(wast_raw),
+ Ok(()),
+ );
+ continue;
+ }
+ wast::WastExecute::Invoke(invoke) => Ok(invoke),
+ } {
+ Ok(invoke) => invoke,
+ Err(err) => {
+ test_group.add_result(
+ &format!("AssertReturn(unsupported-{i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("unsupported directive: {err:?}")),
+ );
+ continue;
+ }
+ };
+
+ let invoke_name = invoke.name;
+ let res: Result<Result<()>, _> = catch_unwind_silent(|| {
+ let args = convert_wastargs(invoke.args)?;
+ let module = module_registry.get_idx(invoke.module);
+ let outcomes = exec_fn_instance(module, &mut store, invoke.name, &args).map_err(|e| {
+ error!("failed to execute function: {e:?}");
+ e
+ })?;
+ if !expected_alternatives.iter().any(|expected| expected.len() == outcomes.len()) {
+ return Err(eyre!(
+ "expected {} results, got {}",
+ expected_alternatives.first().map_or(0, |v| v.len()),
+ outcomes.len()
+ ));
+ }
+ if expected_alternatives.iter().any(|expected| {
+ expected.len() == outcomes.len()
+ && outcomes.iter().zip(expected.iter()).all(|(outcome, exp)| outcome.eq_loose(exp))
+ }) {
+ Ok(())
+ } else {
+ Err(eyre!("results did not match any expected alternative"))
+ }
+ });
+
+ let res = res.map_err(|e| eyre!("test panicked: {}", try_downcast_panic(e))).and_then(|r| r);
+ test_group.add_result(&format!("AssertReturn({invoke_name}-{i})"), span.linecol_in(wast_raw), res);
+ }
+ _ => test_group.add_result(
+ &format!("Unknown({i})"),
+ span.linecol_in(wast_raw),
+ Err(eyre!("unsupported directive")),
+ ),
+ }
+ }
+
+ Ok(())
+ }
+}
+
+impl Display for WastRunner {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ use owo_colors::OwoColorize;
+
+ let mut total_passed = 0;
+ let mut total_failed = 0;
+
+ for group in self.group_results() {
+ total_passed += group.passed;
+ total_failed += group.failed;
+
+ writeln!(f, "{}", group.name.bold().underline())?;
+ writeln!(f, " Tests Passed: {}", group.passed.to_string().green())?;
+ if group.failed != 0 {
+ writeln!(f, " Tests Failed: {}", group.failed.to_string().red())?;
+ }
+ }
+
+ writeln!(f, "\n{}", "Total Test Summary:".bold().underline())?;
+ writeln!(f, " Total Tests: {}", total_passed + total_failed)?;
+ writeln!(f, " Total Passed: {}", total_passed.to_string().green())?;
+ writeln!(f, " Total Failed: {}", total_failed.to_string().red())?;
+ Ok(())
+ }
+}
+
+#[derive(Debug)]
+struct TestGroup {
+ tests: Vec<TestCase>,
+ file: String,
+}
+
+impl TestGroup {
+ fn new(file: &str) -> Self {
+ Self { tests: Vec::new(), file: file.to_string() }
+ }
+
+ fn stats(&self) -> (usize, usize) {
+ let mut passed = 0;
+ let mut failed = 0;
+ for test in &self.tests {
+ match test.result {
+ Ok(()) => passed += 1,
+ Err(_) => failed += 1,
+ }
+ }
+ (passed, failed)
+ }
+
+ fn add_result(&mut self, name: &str, linecol: (usize, usize), result: Result<()>) {
+ self.tests.push(TestCase { name: name.to_string(), linecol, result });
+ }
+}
+
+#[derive(Debug)]
+struct TestCase {
+ name: String,
+ linecol: (usize, usize),
+ result: Result<()>,
+}
+
+fn expand_paths(paths: &[PathBuf]) -> Result<Vec<PathBuf>> {
+ let mut files = Vec::new();
+ for path in paths {
+ if path.is_dir() {
+ for entry in std::fs::read_dir(path)? {
+ let entry = entry?;
+ let path = entry.path();
+ if path.extension().is_some_and(|ext| ext == "wast") {
+ files.push(path);
+ }
+ }
+ } else {
+ files.push(path.clone());
+ }
+ }
+ files.sort();
+ Ok(files)
+}
+
+#[derive(Debug)]
+pub struct TestFile<'a> {
+ pub name: String,
+ pub contents: &'a str,
+ pub parent: String,
+}
+
+impl<'a> TestFile<'a> {
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+
+ pub fn raw(&self) -> &'a str {
+ self.contents
+ }
+
+ pub fn parent(&self) -> &str {
+ &self.parent
+ }
+
+ pub fn wast(&self) -> wast::parser::Result<WastBuffer<'a>> {
+ let mut lexer = wast::lexer::Lexer::new(self.contents);
+ lexer.allow_confusing_unicode(true);
+ let parse_buffer = wast::parser::ParseBuffer::new_with_lexer(lexer)?;
+ Ok(WastBuffer { buffer: parse_buffer })
+ }
+}
+
+pub struct WastBuffer<'a> {
+ buffer: wast::parser::ParseBuffer<'a>,
+}
+
+impl<'a> WastBuffer<'a> {
+ pub fn directives(&'a self) -> wast::parser::Result<Vec<wast::WastDirective<'a>>> {
+ Ok(wast::parser::parse::<wast::Wast<'a>>(&self.buffer)?.directives)
+ }
+}
+
+fn exec_with_budget(
+ func: &tinywasm::Function,
+ store: &mut Store,
+ args: &[WasmValue],
+) -> Result<Vec<WasmValue>, tinywasm::Error> {
+ let mut exec = func.call_resumable(store, args)?;
+ for _ in 0..TEST_MAX_SUSPENSIONS {
+ match exec.resume_with_time_budget(TEST_TIME_SLICE)? {
+ ExecProgress::Completed(values) => return Ok(values),
+ ExecProgress::Suspended => {}
+ }
+ }
+ Err(tinywasm::Error::Other(format!(
+ "testsuite execution timed out after {} time slices of {:?}",
+ TEST_MAX_SUSPENSIONS, TEST_TIME_SLICE
+ )))
+}
+
+fn try_downcast_panic(panic: Box<dyn std::any::Any + Send>) -> String {
+ let info = panic.downcast_ref::<panic::PanicHookInfo>().map(ToString::to_string);
+ let info_string = panic.downcast_ref::<String>().cloned();
+ let info_str = panic.downcast::<&str>().ok().map(|s| *s);
+ info.unwrap_or_else(|| info_str.unwrap_or(&info_string.unwrap_or("unknown panic".to_owned())).to_string())
+}
+
+fn exec_fn_instance(
+ instance: Option<u32>,
+ store: &mut Store,
+ name: &str,
+ args: &[WasmValue],
+) -> Result<Vec<WasmValue>, tinywasm::Error> {
+ let Some(instance) = instance else {
+ return Err(tinywasm::Error::Other("no instance found".to_string()));
+ };
+ let Some(instance) = store.get_module_instance(instance) else {
+ return Err(tinywasm::Error::Other("no instance found".to_string()));
+ };
+ let func = instance.func_untyped(store, name)?;
+ exec_with_budget(&func, store, args)
+}
+
+fn catch_unwind_silent<R>(f: impl FnOnce() -> R) -> std::thread::Result<R> {
+ let prev_hook = panic::take_hook();
+ panic::set_hook(Box::new(|_| {}));
+ let result = panic::catch_unwind(AssertUnwindSafe(f));
+ panic::set_hook(prev_hook);
+ result
+}
+
+fn encode_quote_wat(module: QuoteWat) -> (Option<String>, Vec<u8>) {
+ match module {
+ QuoteWat::QuoteModule(_, quoted_wat) => {
+ let wat = quoted_wat
+ .iter()
+ .map(|(_, s)| std::str::from_utf8(s).expect("failed to convert wast to utf8"))
+ .collect::<Vec<_>>()
+ .join("\n");
+ let lexer = wast::lexer::Lexer::new(&wat);
+ let buf = wast::parser::ParseBuffer::new_with_lexer(lexer).expect("failed to create parse buffer");
+ let mut wat_data = wast::parser::parse::<wast::Wat>(&buf).expect("failed to parse wat");
+ (None, wat_data.encode().expect("failed to encode module"))
+ }
+ QuoteWat::Wat(mut wat) => {
+ let wast::Wat::Module(ref module) = wat else { unimplemented!("Not supported") };
+ (module.id.map(|id| id.name().to_string()), wat.encode().expect("failed to encode module"))
+ }
+ QuoteWat::QuoteComponent(..) => unimplemented!("components are not supported"),
+ }
+}
+
+fn parse_module_bytes(bytes: &[u8]) -> Result<Module> {
+ Ok(tinywasm::parse_bytes(bytes)?)
+}
+
+fn convert_wastargs(args: Vec<wast::WastArg>) -> Result<Vec<WasmValue>> {
+ args.into_iter().map(wastarg2tinywasmvalue).collect()
+}
+
+fn convert_wastret<'a>(args: impl Iterator<Item = wast::WastRet<'a>>) -> Result<Vec<Vec<WasmValue>>> {
+ let mut alternatives = vec![Vec::new()];
+ for arg in args {
+ let choices = wastret2tinywasmvalues(arg)?;
+ let mut next = Vec::with_capacity(alternatives.len() * choices.len());
+ for prefix in alternatives {
+ for choice in &choices {
+ let mut candidate = prefix.clone();
+ candidate.push(*choice);
+ next.push(candidate);
+ }
+ }
+ alternatives = next;
+ }
+ Ok(alternatives)
+}
+
+fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result<WasmValue> {
+ let wast::WastArg::Core(arg) = arg else { bail!("unsupported arg type: Component") };
+ use wast::core::WastArgCore::*;
+ Ok(match arg {
+ F32(f) => WasmValue::F32(f32::from_bits(f.bits)),
+ F64(f) => WasmValue::F64(f64::from_bits(f.bits)),
+ I32(i) => WasmValue::I32(i),
+ I64(i) => WasmValue::I64(i),
+ V128(i) => WasmValue::V128(i128::from_le_bytes(i.to_le_bytes())),
+ RefExtern(v) => WasmValue::RefExtern(ExternRef::new(Some(v))),
+ RefNull(t) => match t {
+ wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Func } => {
+ WasmValue::RefFunc(FuncRef::null())
+ }
+ wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Extern } => {
+ WasmValue::RefExtern(ExternRef::null())
+ }
+ _ => bail!("unsupported arg type: refnull: {:?}", t),
+ },
+ RefHost(_) => bail!("unsupported arg type: RefHost"),
+ })
+}
+
+fn wast_i128_to_i128(i: wast::core::V128Pattern) -> i128 {
+ let res: Vec<u8> = match i {
+ wast::core::V128Pattern::F32x4(f) => {
+ f.iter().flat_map(|v| nanpattern2tinywasmvalue(*v).unwrap().as_f32().unwrap().to_le_bytes()).collect()
+ }
+ wast::core::V128Pattern::F64x2(f) => {
+ f.iter().flat_map(|v| nanpattern2tinywasmvalue(*v).unwrap().as_f64().unwrap().to_le_bytes()).collect()
+ }
+ wast::core::V128Pattern::I16x8(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(),
+ wast::core::V128Pattern::I32x4(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(),
+ wast::core::V128Pattern::I64x2(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(),
+ wast::core::V128Pattern::I8x16(f) => f.iter().flat_map(|v| v.to_le_bytes()).collect(),
+ };
+ i128::from_le_bytes(res.try_into().unwrap())
+}
+
+fn wastret2tinywasmvalues(ret: wast::WastRet) -> Result<Vec<WasmValue>> {
+ let wast::WastRet::Core(ret) = ret else { bail!("unsupported arg type") };
+ match ret {
+ wast::core::WastRetCore::Either(options) => {
+ options.into_iter().map(wastretcore2tinywasmvalue).collect::<Result<Vec<_>>>()
+ }
+ ret => Ok(vec![wastretcore2tinywasmvalue(ret)?]),
+ }
+}
+
+fn wastretcore2tinywasmvalue(ret: wast::core::WastRetCore) -> Result<WasmValue> {
+ use wast::core::WastRetCore::{F32, F64, I32, I64, RefExtern, RefFunc, RefNull, V128};
+ Ok(match ret {
+ F32(f) => nanpattern2tinywasmvalue(f)?,
+ F64(f) => nanpattern2tinywasmvalue(f)?,
+ I32(i) => WasmValue::I32(i),
+ I64(i) => WasmValue::I64(i),
+ V128(i) => WasmValue::V128(wast_i128_to_i128(i)),
+ RefNull(t) => match t {
+ Some(wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Func }) => {
+ WasmValue::RefFunc(FuncRef::null())
+ }
+ Some(wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Extern }) => {
+ WasmValue::RefExtern(ExternRef::null())
+ }
+ _ => bail!("unsupported arg type: refnull: {:?}", t),
+ },
+ RefExtern(v) => WasmValue::RefExtern(ExternRef::new(v)),
+ RefFunc(v) => WasmValue::RefFunc(FuncRef::new(match v {
+ Some(wast::token::Index::Num(n, _)) => Some(n),
+ _ => bail!("unsupported arg type: reffunc: {:?}", v),
+ })),
+ a => bail!("unsupported arg type {:?}", a),
+ })
+}
+
+enum Bits {
+ U32(u32),
+ U64(u64),
+}
+
+trait FloatToken {
+ fn bits(&self) -> Bits;
+ fn canonical_nan() -> WasmValue;
+ fn arithmetic_nan() -> WasmValue;
+ fn value(&self) -> WasmValue {
+ match self.bits() {
+ Bits::U32(v) => WasmValue::F32(f32::from_bits(v)),
+ Bits::U64(v) => WasmValue::F64(f64::from_bits(v)),
+ }
+ }
+}
+
+impl FloatToken for wast::token::F32 {
+ fn bits(&self) -> Bits {
+ Bits::U32(self.bits)
+ }
+ fn canonical_nan() -> WasmValue {
+ WasmValue::F32(f32::NAN)
+ }
+ fn arithmetic_nan() -> WasmValue {
+ WasmValue::F32(f32::NAN)
+ }
+}
+
+impl FloatToken for wast::token::F64 {
+ fn bits(&self) -> Bits {
+ Bits::U64(self.bits)
+ }
+ fn canonical_nan() -> WasmValue {
+ WasmValue::F64(f64::NAN)
+ }
+ fn arithmetic_nan() -> WasmValue {
+ WasmValue::F64(f64::NAN)
+ }
+}
+
+fn nanpattern2tinywasmvalue<T>(arg: wast::core::NanPattern<T>) -> Result<WasmValue>
+where
+ T: FloatToken,
+{
+ use wast::core::NanPattern::{ArithmeticNan, CanonicalNan, Value};
+ Ok(match arg {
+ CanonicalNan => T::canonical_nan(),
+ ArithmeticNan => T::arithmetic_nan(),
+ Value(v) => v.value(),
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn runs_simple_wast_file() {
+ let dir = tempfile::tempdir().unwrap();
+ let path = dir.path().join("simple.wast");
+ std::fs::write(
+ &path,
+ "(module (func (export \"add\") (result i32) i32.const 1))\n(assert_return (invoke \"add\") (i32.const 1))",
+ )
+ .unwrap();
+
+ let mut runner = WastRunner::new();
+ runner.run_paths(&[path]).unwrap();
+ }
+}
diff --git a/crates/cli/src/wat.rs b/crates/cli/src/wat.rs
deleted file mode 100644
index 8d2998d..0000000
--- a/crates/cli/src/wat.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-use wast::{
- Wat,
- parser::{self, ParseBuffer},
-};
-
-pub fn wat2wasm(wat: &str) -> Vec<u8> {
- let buf = ParseBuffer::new(wat).expect("failed to create parse buffer");
- let mut module = parser::parse::<Wat>(&buf).expect("failed to parse wat");
- module.encode().expect("failed to encode wat")
-}
diff --git a/crates/cli/tests/cli.rs b/crates/cli/tests/cli.rs
new file mode 100644
index 0000000..b7d303e
--- /dev/null
+++ b/crates/cli/tests/cli.rs
@@ -0,0 +1,137 @@
+use std::fs;
+
+use assert_cmd::Command;
+use predicates::prelude::*;
+use tempfile::tempdir;
+
+fn write_module(dir: &tempfile::TempDir, name: &str, source: &str) -> String {
+ let path = dir.path().join(name);
+ fs::write(&path, source).unwrap();
+ path.to_string_lossy().into_owned()
+}
+
+#[test]
+fn run_invoke_accepts_positional_args() {
+ let dir = tempdir().unwrap();
+ let module = write_module(
+ &dir,
+ "add.wat",
+ r#"(module
+ (func (export "add") (param i32 i32) (result i32)
+ local.get 0
+ local.get 1
+ i32.add))"#,
+ );
+
+ Command::cargo_bin("tinywasm")
+ .unwrap()
+ .args(["run", "--invoke", "add", &module, "1", "2"])
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("i32(3)"));
+}
+
+#[test]
+fn compile_and_run_twasm() {
+ let dir = tempdir().unwrap();
+ let input = write_module(
+ &dir,
+ "add.wat",
+ r#"(module
+ (func (export "add") (param i32 i32) (result i32)
+ local.get 0
+ local.get 1
+ i32.add))"#,
+ );
+ let output = dir.path().join("add.twasm");
+
+ Command::cargo_bin("tinywasm")
+ .unwrap()
+ .args(["compile", &input, "-o", output.to_str().unwrap()])
+ .assert()
+ .success();
+
+ Command::cargo_bin("tinywasm")
+ .unwrap()
+ .args(["run", "--invoke", "add", output.to_str().unwrap(), "3", "4"])
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("i32(7)"));
+}
+
+#[test]
+fn bare_run_requires_default_entrypoint() {
+ let dir = tempdir().unwrap();
+ let module = write_module(&dir, "add.wat", r#"(module (func (export "add") (result i32) i32.const 1))"#);
+
+ Command::cargo_bin("tinywasm")
+ .unwrap()
+ .arg(&module)
+ .assert()
+ .failure()
+ .stderr(predicate::str::contains("no start function or `_start` export"));
+}
+
+#[test]
+fn inspect_lists_exports() {
+ let dir = tempdir().unwrap();
+ let module = write_module(
+ &dir,
+ "inspect.wat",
+ r#"(module
+ (memory (export "memory") 1)
+ (func (export "answer") (result i32) i32.const 42))"#,
+ );
+
+ Command::cargo_bin("tinywasm").unwrap().args(["inspect", &module]).assert().success().stdout(
+ predicate::str::contains("answer: func () -> (i32)").and(predicate::str::contains("memory: memory[i32]")),
+ );
+}
+
+#[test]
+fn dump_prints_lowered_instructions() {
+ let dir = tempdir().unwrap();
+ let module = write_module(&dir, "dump.wat", r#"(module (func (export "noop")))"#);
+
+ Command::cargo_bin("tinywasm")
+ .unwrap()
+ .args(["dump", &module])
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("func[0]").and(predicate::str::contains("0000:")));
+}
+
+#[test]
+fn run_accepts_wat_from_stdin() {
+ Command::cargo_bin("tinywasm")
+ .unwrap()
+ .args(["run", "--invoke", "add", "-", "8", "9"])
+ .write_stdin(
+ r#"(module
+ (func (export "add") (param i32 i32) (result i32)
+ local.get 0
+ local.get 1
+ i32.add))"#,
+ )
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("i32(17)"));
+}
+
+#[test]
+fn wast_command_runs_simple_spec_script() {
+ let dir = tempdir().unwrap();
+ let path = dir.path().join("simple.wast");
+ fs::write(
+ &path,
+ "(module (func (export \"add\") (result i32) i32.const 1))\n(assert_return (invoke \"add\") (i32.const 1))",
+ )
+ .unwrap();
+
+ Command::cargo_bin("tinywasm")
+ .unwrap()
+ .args(["wast", path.to_str().unwrap()])
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("Tests Passed:"));
+}