diff --git a/Cargo.lock b/Cargo.lock index 06e6124..3784148 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -252,6 +261,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.5.60" @@ -307,6 +322,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "console" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -516,6 +544,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + [[package]] name = "debug" version = "0.1.0" @@ -533,6 +572,25 @@ dependencies = [ "uuid", ] +[[package]] +name = "dialog" +version = "0.1.0" +dependencies = [ + "console", + "dialoguer", + "wasm-wave 0.239.0", +] + +[[package]] +name = "dialoguer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +dependencies = [ + "console", + "shell-words", +] + [[package]] name = "digest" version = "0.10.7" @@ -564,6 +622,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -593,6 +663,12 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -1225,6 +1301,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "object" version = "0.37.3" @@ -1332,6 +1435,8 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "ctrlc", + "dialog", "heck", "prettyplease", "proc-macro2", @@ -1628,6 +1733,12 @@ dependencies = [ "digest", ] +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" @@ -2093,6 +2204,16 @@ dependencies = [ "wasmparser 0.245.1", ] +[[package]] +name = "wasm-wave" +version = "0.239.0" +source = "git+https://github.com/chenyan2002/wasm-tools.git?branch=extend-wave#efecaa186fc3c3abe637bcad6234403b28d09322" +dependencies = [ + "indexmap", + "logos", + "thiserror 2.0.18", +] + [[package]] name = "wasm-wave" version = "0.244.0" @@ -2176,7 +2297,7 @@ dependencies = [ "tempfile", "wasm-compose", "wasm-encoder 0.244.0", - "wasm-wave", + "wasm-wave 0.244.0", "wasmparser 0.244.0", "wasmtime-environ", "wasmtime-internal-cache", diff --git a/Cargo.toml b/Cargo.toml index c57f860..03056ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,12 @@ members = [ "components/debug", "components/recorder" -, "crates/trace"] +, "crates/dialog", "crates/trace"] [workspace.dependencies] wit-bindgen = { version = "0.52.0", default-features = false, features = ["bitflags", "std", "macros"] } serde = { version = "1.0.226", features = ["derive"] } serde_json = "1.0.145" +wasm-wave = { git = "https://github.com/chenyan2002/wasm-tools.git", branch = "extend-wave", version = "0.239.0", default-features = false } [package] name = "proxy-component" @@ -28,7 +29,9 @@ wit-component = "0.245.0" wasmtime = { version = "42.0.0", features = ["wave"], optional = true } wasmtime-wasi = { version = "42.0.0", optional = true } +dialog = { path = "crates/dialog", optional = true } +ctrlc = "3.5.2" [features] -run = ["wasmtime", "wasmtime-wasi"] -#default = ["run"] +run = ["wasmtime", "wasmtime-wasi", "dialog"] +default = ["run"] diff --git a/Makefile b/Makefile index 8fa07f1..2ca7992 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all build-components build-cli test test-record test-fuzz run-fuzz run-record run-viceroy +.PHONY: all build-components build-cli test test-record test-fuzz test-dialog run-fuzz run-record run-dialog run-viceroy all: build-components build-cli build-cli: @@ -9,10 +9,14 @@ build-components: cp target/wasm32-wasip2/release/debug.wasm assets/debug.wasm cp target/wasm32-wasip2/release/recorder.wasm assets/recorder.wasm -test: test-fuzz test-record +test: test-fuzz test-record test-dialog test-fuzz: - RUSTFLAGS="" $(MAKE) run-fuzz WASM=tests/calculator.wasm + $(MAKE) run-fuzz WASM=tests/calculator.wasm + # build-only test + target/release/proxy-component instrument -m fuzz tests/rust.wasm + target/release/proxy-component instrument -m fuzz tests/go.wasm + target/release/proxy-component instrument -m fuzz tests/python.wasm test-record: $(MAKE) run-record WASM=tests/go.wasm @@ -21,6 +25,15 @@ test-record: # test the same trace with a different wasm replay target/release/proxy-component instrument -m replay tests/rust.debug.wasm wasmtime --invoke 'start()' composed.wasm < trace.out + # build-only test + target/release/proxy-component instrument -m record tests/calculator.wasm + target/release/proxy-component instrument -m replay tests/calculator.wasm + +test-dialog: + rm tests/composed.wasm || true + for wasm in tests/*.wasm; do \ + $(MAKE) run-dialog WASM=$$wasm; \ + done run-fuzz: target/release/proxy-component instrument -m fuzz $(WASM) @@ -35,6 +48,11 @@ run-record: target/release/proxy-component instrument -m replay --use-host-recorder $(WASM) target/release/proxy-component run composed.wasm --invoke 'start()' --trace trace.out +run-dialog: + target/release/proxy-component instrument -m dialog $(WASM) + # build-only + # target/release/proxy-component run composed.wasm --invoke 'start()' + run-viceroy: viceroy composed.wasm > trace.out & echo $$! > viceroy.pid until nc -z localhost 7676; do \ diff --git a/README.md b/README.md index 31a5175..08be216 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,19 @@ $ wasmtime --invoke 'start()' composed.wasm Fuzzing the import return and export input based on the WIT type. This mode requires a [Debug component](components/debug/) to get random numbers and logging. The `composed.wasm` can be run in a standalone `wasmtime` without any special host functions. +### Dialog + +``` +$ proxy-component instrument -m dialog +$ proxy-component run composed.wasm --invoke 'start()' +``` + +Provide an interactive terminal for users to mock import and export calls. +[![asciicast](https://asciinema.org/a/BeVtK4cwsTsqmDo6.svg)](https://asciinema.org/a/BeVtK4cwsTsqmDo6) + +This mode requires raw access to terminal, which can only be implemented on the host side for now. +So the composed binary can only be run with `proxy-component run`, instead of a regular `wasmtime`. + ### Generate Given a `bindings.rs` file generated from `wit-bindgen`. This command can generate code to implement diff --git a/assets/util.wit b/assets/util.wit index 661dd04..5e9ca6d 100644 --- a/assets/util.wit +++ b/assets/util.wit @@ -6,6 +6,31 @@ interface debug { get-random: func() -> list; } +interface dialog { + print: func(dep:u32, x:string); + read-raw-string: func(dep: u32, prompt: string) -> string; + read-num: func(dep: u32, prompt: string) -> u32; + read-select: func(dep: u32, prompt: string, items: list) -> u32; + read-multi-select: func(dep: u32, prompt: string, items: list) -> list; + read-bool: func(dep: u32) -> string; + read-string: func(dep: u32) -> string; + read-u8: func(dep: u32) -> string; + read-u16: func(dep: u32) -> string; + read-u32: func(dep: u32) -> string; + read-u64: func(dep: u32) -> string; + read-s8: func(dep: u32) -> string; + read-s16: func(dep: u32) -> string; + read-s32: func(dep: u32) -> string; + read-s64: func(dep: u32) -> string; + read-f32: func(dep: u32) -> string; + read-f64: func(dep: u32) -> string; + read-char: func(dep: u32) -> string; +} + world crate-debug { export debug; } + +world host-dialog { + import dialog; +} \ No newline at end of file diff --git a/crates/dialog/Cargo.toml b/crates/dialog/Cargo.toml new file mode 100644 index 0000000..f7c38f4 --- /dev/null +++ b/crates/dialog/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "dialog" +version = "0.1.0" +edition = "2024" + +[dependencies] +console = "0.16.2" +dialoguer = { version = "0.12.0", default-features = false } +wasm-wave.workspace = true diff --git a/crates/dialog/src/lib.rs b/crates/dialog/src/lib.rs new file mode 100644 index 0000000..6bc580a --- /dev/null +++ b/crates/dialog/src/lib.rs @@ -0,0 +1,303 @@ +use console::{Style, StyledObject, style}; +use dialoguer::{Input, Select, theme::Theme}; +use std::fmt; +use wasm_wave::value::convert::ToValue; + +pub use console; + +pub fn print(dep: u32, message: &str) { + let theme = IndentTheme::new(dep as usize); + theme.println(message); +} + +pub fn read_string(dep: u32) -> String { + let theme = IndentTheme::new(dep as usize); + let text = Input::::with_theme(&theme) + .allow_empty(true) + .with_prompt("Enter a string") + .interact() + .unwrap(); + wasm_wave::to_string(&text.to_value()).unwrap() +} +pub fn read_bool(dep: u32) -> String { + let theme = IndentTheme::new(dep as usize); + let selection = Select::with_theme(&theme) + .with_prompt("Select a bool") + .items(&["true", "false"]) + .interact() + .unwrap(); + let value = if selection == 0 { true } else { false }; + wasm_wave::to_string(&value.to_value()).unwrap() +} +pub fn read_num(dep: u32, prompt: String) -> u32 { + let theme = IndentTheme::new(dep as usize); + let num = Input::::with_theme(&theme) + .with_prompt(prompt) + .interact_text() + .unwrap(); + num +} +pub fn read_raw_string(dep: u32, prompt: String) -> String { + let theme = IndentTheme::new(dep as usize); + let text = Input::::with_theme(&theme) + .allow_empty(true) + .with_prompt(prompt) + .interact() + .unwrap(); + text +} +pub fn read_select(dep: u32, prompt: String, items: Vec) -> u32 { + let theme = IndentTheme::new(dep as usize); + let selection = Select::with_theme(&theme) + .with_prompt(prompt) + .items(&items) + .default(0) + .interact() + .unwrap(); + selection as u32 +} +pub fn read_multi_select(dep: u32, prompt: String, items: Vec) -> Vec { + let theme = IndentTheme::new(dep as usize); + let selections = dialoguer::MultiSelect::with_theme(&theme) + .with_prompt(prompt) + .items(&items) + .interact() + .unwrap(); + selections.into_iter().map(|s| s as u32).collect() +} +macro_rules! read_primitive { + ($fn_name:ident, $ty:ty, $prompt:expr) => { + pub fn $fn_name(dep: u32) -> String { + let theme = IndentTheme::new(dep as usize); + let num = Input::<$ty>::with_theme(&theme) + .with_prompt($prompt) + .interact_text() + .unwrap(); + wasm_wave::to_string(&num.to_value()).unwrap() + } + }; +} + +read_primitive!(read_u8, u8, "Enter a u8"); +read_primitive!(read_u16, u16, "Enter a u16"); +read_primitive!(read_u32, u32, "Enter a u32"); +read_primitive!(read_u64, u64, "Enter a u64"); +read_primitive!(read_s8, i8, "Enter a s8"); +read_primitive!(read_s16, i16, "Enter a s16"); +read_primitive!(read_s32, i32, "Enter a s32"); +read_primitive!(read_s64, i64, "Enter a s64"); +read_primitive!(read_f32, f32, "Enter a f32"); +read_primitive!(read_f64, f64, "Enter a f64"); +read_primitive!(read_char, char, "Enter a char"); + +pub struct IndentTheme { + indent: usize, + defaults_style: Style, + prompt_style: Style, + prompt_prefix: StyledObject, + prompt_suffix: StyledObject, + success_prefix: StyledObject, + success_suffix: StyledObject, + error_prefix: StyledObject, + error_style: Style, + hint_style: Style, + values_style: Style, + active_item_style: Style, + inactive_item_style: Style, + active_item_prefix: StyledObject, + inactive_item_prefix: StyledObject, +} +impl IndentTheme { + pub fn new(indent: usize) -> Self { + Self { + indent, + defaults_style: Style::new().for_stderr().cyan(), + prompt_style: Style::new().for_stderr().bold(), + prompt_prefix: style("?".to_string()).for_stderr().yellow(), + prompt_suffix: style("›".to_string()).for_stderr().black().bright(), + success_prefix: style("✔".to_string()).for_stderr().green(), + success_suffix: style("·".to_string()).for_stderr().black().bright(), + error_prefix: style("✘".to_string()).for_stderr().red(), + error_style: Style::new().for_stderr().red(), + hint_style: Style::new().for_stderr().black().bright(), + values_style: Style::new().for_stderr().green(), + active_item_style: Style::new().for_stderr().cyan(), + inactive_item_style: Style::new().for_stderr(), + active_item_prefix: style("❯".to_string()).for_stderr().green(), + inactive_item_prefix: style(" ".to_string()).for_stderr(), + } + } + pub fn indent(&self, f: &mut dyn fmt::Write) -> fmt::Result { + let spaces = " ".repeat(self.indent * 2); + write!(f, "{spaces}") + } + pub fn println(&self, prompt: &str) { + let spaces = " ".repeat(self.indent * 2); + println!("{spaces}{prompt}"); + } + pub fn hint(&self, prompt: &str) { + let spaces = " ".repeat(self.indent * 2); + println!("{spaces}{}", self.hint_style.apply_to(prompt)); + } +} +impl Theme for IndentTheme { + fn format_prompt(&self, f: &mut dyn fmt::Write, prompt: &str) -> fmt::Result { + self.indent(f)?; + write!( + f, + "{} {} ", + &self.prompt_prefix, + self.prompt_style.apply_to(prompt) + )?; + write!(f, "{}", &self.prompt_suffix) + } + fn format_error(&self, f: &mut dyn fmt::Write, err: &str) -> fmt::Result { + self.indent(f)?; + write!( + f, + "{} {}", + &self.error_prefix, + self.error_style.apply_to(err) + ) + } + fn format_input_prompt( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + default: Option<&str>, + ) -> fmt::Result { + self.indent(f)?; + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.prompt_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + + match default { + Some(default) => write!( + f, + "{} {} ", + self.hint_style.apply_to(&format!("({})", default)), + &self.prompt_suffix + ), + None => write!(f, "{} ", &self.prompt_suffix), + } + } + fn format_confirm_prompt( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + default: Option, + ) -> fmt::Result { + self.indent(f)?; + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.prompt_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + + match default { + None => write!( + f, + "{} {}", + self.hint_style.apply_to("(y/n)"), + &self.prompt_suffix + ), + Some(true) => write!( + f, + "{} {} {}", + self.hint_style.apply_to("(y/n)"), + &self.prompt_suffix, + self.defaults_style.apply_to("yes") + ), + Some(false) => write!( + f, + "{} {} {}", + self.hint_style.apply_to("(y/n)"), + &self.prompt_suffix, + self.defaults_style.apply_to("no") + ), + } + } + fn format_confirm_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + selection: Option, + ) -> fmt::Result { + self.indent(f)?; + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.success_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + let selection = selection.map(|b| if b { "yes" } else { "no" }); + + match selection { + Some(selection) => { + write!( + f, + "{} {}", + &self.success_suffix, + self.values_style.apply_to(selection) + ) + } + None => { + write!(f, "{}", &self.success_suffix) + } + } + } + fn format_input_prompt_selection( + &self, + f: &mut dyn fmt::Write, + prompt: &str, + sel: &str, + ) -> fmt::Result { + self.indent(f)?; + if !prompt.is_empty() { + write!( + f, + "{} {} ", + &self.success_prefix, + self.prompt_style.apply_to(prompt) + )?; + } + + write!( + f, + "{} {}", + &self.success_suffix, + self.values_style.apply_to(sel) + ) + } + fn format_select_prompt_item( + &self, + f: &mut dyn fmt::Write, + text: &str, + active: bool, + ) -> fmt::Result { + self.indent(f)?; + let details = if active { + ( + &self.active_item_prefix, + self.active_item_style.apply_to(text), + ) + } else { + ( + &self.inactive_item_prefix, + self.inactive_item_style.apply_to(text), + ) + }; + + write!(f, "{} {}", details.0, details.1) + } +} diff --git a/src/ast.rs b/src/ast.rs index 572e53a..50f2ace 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -55,6 +55,9 @@ impl<'a> Opt<'a> { Mode::Fuzz => { out.push_str(&format!("import proxy:util/debug;\n")); } + Mode::Dialog => { + out.push_str(&format!("import proxy:util/dialog;\n")); + } }; out.push_str("export proxy:conversion/conversion;\n"); for (name, import) in &world.imports { @@ -73,7 +76,9 @@ impl<'a> Opt<'a> { out.push_str(&format!("import {name};\n")); out.push_str(&format!("export wrapped-{name};\n")); } - Mode::Replay | Mode::Fuzz => out.push_str(&format!("export {name};\n")), + Mode::Replay | Mode::Fuzz | Mode::Dialog => { + out.push_str(&format!("export {name};\n")) + } } } _ => todo!(), @@ -91,7 +96,7 @@ impl<'a> Opt<'a> { out.push_str(&format!("import wrapped-{name};\n")); out.push_str(&format!("export {name};\n")); } - Mode::Replay | Mode::Fuzz => { + Mode::Replay | Mode::Fuzz | Mode::Dialog => { out.push_str(&format!("import {name};\n")); } } @@ -99,7 +104,7 @@ impl<'a> Opt<'a> { _ => todo!(), } } - if matches!(self.mode, Mode::Replay | Mode::Fuzz) { + if matches!(self.mode, Mode::Replay | Mode::Fuzz | Mode::Dialog) { out.push_str("export proxy:recorder/start-replay@0.1.0;\n") } out.push_str("}\n"); @@ -120,6 +125,9 @@ impl<'a> Opt<'a> { Mode::Fuzz => { out.push_str(&format!("import proxy:util/debug;\n")); } + Mode::Dialog => { + out.push_str(&format!("import proxy:util/dialog;\n")); + } }; out.push_str("import proxy:conversion/conversion;\n"); for (name, import) in &world.imports { @@ -303,7 +311,7 @@ impl<'a> Opt<'a> { "get-host-{func_name}: func(x: wrapped-{func_name}) -> host-{func_name};\n", )); } - Mode::Replay | Mode::Fuzz => { + Mode::Replay | Mode::Fuzz | Mode::Dialog => { // Add a magic separator so that codegen::generate_conversion_func can recover the resource name let magic_name = format!("{iface_no_ver}-magic42-{resource}").to_kebab_case(); out.push_str(&format!("\nuse {iface}.{{{resource} as {func_name}}};\n")); @@ -349,6 +357,7 @@ impl<'a> Opt<'a> { let name = resolve.name_world_key(name); let link_type = match name.as_str() { "proxy:util/debug" => LinkType::Debug, + "proxy:util/dialog" => LinkType::Host, name if name.starts_with("proxy:recorder/") => LinkType::Recorder, _ => LinkType::Host, }; @@ -369,6 +378,7 @@ impl<'a> Opt<'a> { let link_type = match name.as_str() { "proxy:util/debug" => LinkType::Debug, "proxy:conversion/conversion" => LinkType::Imports, + "proxy:util/dialog" => LinkType::Host, name if name.starts_with("proxy:recorder/") => LinkType::Recorder, name if matches!(self.mode, Mode::Record) => { if let Some(stripped) = name.strip_prefix("wrapped-") { diff --git a/src/codegen/dialog.rs b/src/codegen/dialog.rs new file mode 100644 index 0000000..ec56b23 --- /dev/null +++ b/src/codegen/dialog.rs @@ -0,0 +1,119 @@ +use super::State; +use crate::util::{ + FullTypePath, ResourceFuncKind, extract_arg_info, get_owned_type, get_return_type, make_path, + wit_func_name, +}; +use quote::quote; +use syn::{Signature, parse_quote, visit_mut::VisitMut}; + +impl State { + pub fn generate_dialog_func( + &self, + module_path: &[String], + sig: &Signature, + resource: &Option, + ) -> syn::ImplItemFn { + let func_name = &sig.ident; + let is_export = module_path.join("::") == "exports::proxy::recorder::start_replay"; + if !is_export { + let (kind, args) = extract_arg_info(sig); + let arg_names = args.iter().map(|arg| &arg.ident); + let display_name = wit_func_name(module_path, resource, func_name, &kind); + let ret_ty = get_return_type(&sig.output); + if let Some(ty) = ret_ty { + let init_vec = if matches!(kind, Some(ResourceFuncKind::Method)) { + quote! { vec![wasm_wave::to_string(&ToValue::to_value(&self)).unwrap()] } + } else { + quote! { Vec::new() } + }; + parse_quote! { + #sig { + let mut __params: Vec = #init_vec; + #( + __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_names)).unwrap()); + )* + proxy::util::dialog::print(0, &format!("import: {}({})", #display_name, __params.join(", "))); + proxy::util::dialog::print(0, &format!("return type: {:.60}", <#ty as ValueTyped>::value_type().to_string())); + let ret = Dialog::read_value(0); + proxy::util::dialog::print(0, &format!("ret: {}", wasm_wave::to_string(&ToValue::to_value(&ret)).unwrap())); + ret + } + } + } else { + parse_quote! { + #[allow(unused_variables)] + #sig {} + } + } + } else { + assert!(func_name == "start"); + let arms: Vec<_> = self + .funcs + .iter() + .filter(|(path, _)| path[0] != "exports" && path[0] != "proxy") + .flat_map(|(path, resources)| { + resources.iter().flat_map(move |(resource, sigs)| { + sigs.iter().filter_map(move |sig| { + let (kind, args) = extract_arg_info(sig); + if matches!(kind, Some(ResourceFuncKind::Method)) { + return None; + } + let arg_name: Vec<_> = args.iter().map(|arg| &arg.ident).collect(); + let call_param = args.iter().map(|arg| arg.call_param()); + let ty = args.iter().map(|arg| { + let mut ty = arg.ty.clone(); + FullTypePath { module_path: path }.visit_type_mut(&mut ty); + if let Some(owned) = get_owned_type(&ty) { + owned + } else { + ty + } + }); + let func_name = if let Some(resource) = resource { + format!("{}::{}", resource, sig.ident) + } else { + sig.ident.to_string() + }; + let func = make_path(path, &func_name); + let display_name = wit_func_name(path, resource, &sig.ident, &kind); + Some((quote! { + { + proxy::util::dialog::print(0, &format!("call export func {}", #display_name)); + let mut __params: Vec = Vec::new(); + #( + proxy::util::dialog::print(0, &format!("provide argument for {}: {:60}", stringify!(#arg_name), <#ty as ValueTyped>::value_type().to_string())); + let #arg_name: #ty = Dialog::read_value(0); + __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_name)).unwrap()); + )* + proxy::util::dialog::print(0, &format!("export: {}({})", #display_name, __params.join(", "))); + let _ = #func(#(#call_param),*); + } + }, display_name)) + }) + }) + }) + .collect(); + let (arms, display_names): (Vec<_>, Vec<_>) = arms.into_iter().unzip(); + let display_names = + quote! { ["All done".to_string(), #(#display_names.to_string()),*] }; + let func_len = arms.iter().len(); + let idxs = 1..=func_len; + parse_quote! { + #sig { + loop { + let idx = proxy::util::dialog::read_select(0, "Select an export function to call", &#display_names) as usize; + match idx { + 0 => break, + #(#idxs => #arms)* + _ => unreachable!(), + } + // clean up borrowed resources from input args + SCOPED_ALLOC.with(|alloc| { + alloc.borrow_mut().clear(); + }); + } + } + } + } + } +} diff --git a/src/codegen/fuzz.rs b/src/codegen/fuzz.rs index d5bda0b..f0a9c60 100644 --- a/src/codegen/fuzz.rs +++ b/src/codegen/fuzz.rs @@ -21,9 +21,14 @@ impl State { let display_name = wit_func_name(module_path, resource, func_name, &kind); let ret_ty = get_return_type(&sig.output); if ret_ty.is_some() { + let init_vec = if matches!(kind, Some(ResourceFuncKind::Method)) { + quote! { vec![wasm_wave::to_string(&ToValue::to_value(&self)).unwrap()] } + } else { + quote! { Vec::new() } + }; parse_quote! { #sig { - let mut __params: Vec = Vec::new(); + let mut __params: Vec = #init_vec; #( __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_names)).unwrap()); )* @@ -39,6 +44,7 @@ impl State { } } else { parse_quote! { + #[allow(unused_variables)] #sig {} } } diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 8fe8a20..9de3718 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -8,6 +8,7 @@ use syn::{ visit_mut::VisitMut, }; +mod dialog; mod fuzz; mod record; mod replay; @@ -35,6 +36,8 @@ pub enum GenerateMode { Replay, /// A virtualized component with no imports, with implementation for fuzzing. Fuzz, + /// A virtualized component with no imports, with implementation for dialog. + Dialog, } impl GenerateMode { pub fn is_instrument(&self) -> bool { @@ -161,6 +164,9 @@ impl State { self.generate_replay_func(module_path, &sig, &resource) } GenerateMode::Fuzz => self.generate_fuzz_func(module_path, &sig, &resource), + GenerateMode::Dialog => { + self.generate_dialog_func(module_path, &sig, &resource) + } }; methods.push(syn::ImplItem::Fn(stub_impl)); } diff --git a/src/instrument.rs b/src/instrument.rs index 6920933..06fd281 100644 --- a/src/instrument.rs +++ b/src/instrument.rs @@ -15,7 +15,7 @@ pub struct InstrumentArgs { #[arg(short, long)] pub mode: Mode, /// Whether to use the host recorder implementation or link the recorder component - #[arg(short, long)] + #[arg(long)] pub use_host_recorder: bool, } @@ -23,6 +23,9 @@ const DEBUG_WASM: &[u8] = include_bytes!("../assets/debug.wasm"); const RECORDER_WASM: &[u8] = include_bytes!("../assets/recorder.wasm"); pub fn run(args: InstrumentArgs) -> Result<()> { + if args.use_host_recorder && !matches!(args.mode, Mode::Record | Mode::Replay) { + anyhow::bail!("--use-host-recorder only works in record or replay mode"); + } // 1. Create a tmp directory and initialize a new Rust project in it. let tmp_dir = init_rust_project()?; let wit_dir = tmp_dir.join("wit"); @@ -145,6 +148,7 @@ fn bindgen( Mode::Record => codegen::GenerateMode::Record, Mode::Replay => codegen::GenerateMode::Replay, Mode::Fuzz => codegen::GenerateMode::Fuzz, + Mode::Dialog => codegen::GenerateMode::Dialog, }; let codegen_opt = codegen::GenerateArgs { bindings: binding_file.clone(), diff --git a/src/main.rs b/src/main.rs index 4584c9c..f0748eb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ pub enum Mode { Record, Replay, Fuzz, + Dialog, } #[derive(Parser)] @@ -49,6 +50,7 @@ impl Mode { Mode::Record => "record", Mode::Replay => "replay", Mode::Fuzz => "fuzz", + Mode::Dialog => "dialog", } } } diff --git a/src/run.rs b/src/run.rs index 123cdc0..b71dc7c 100644 --- a/src/run.rs +++ b/src/run.rs @@ -26,7 +26,7 @@ mod bindings { }); } -struct State { +pub struct State { wasi_ctx: WasiCtx, resource_table: ResourceTable, logger: Logger, @@ -66,6 +66,11 @@ impl bindings::proxy::recorder::replay::Host for State { const MAX_FUEL: u64 = u64::MAX; pub fn run(args: RunArgs) -> anyhow::Result<()> { + // Patch ctrlc until https://github.com/console-rs/dialoguer/issues/77 is fixed + let _ = ctrlc::try_set_handler(move || { + let term = dialog::console::Term::stdout(); + let _ = term.show_cursor(); + }); let mut config = Config::new(); config .consume_fuel(true) @@ -82,6 +87,10 @@ pub fn run(args: RunArgs) -> anyhow::Result<()> { logger: Logger::new(), exit_called: false, }; + dialog_bindings::proxy::util::dialog::add_to_linker::>( + &mut linker, + |state| state, + )?; if let Some(path) = &args.trace { bindings::proxy::recorder::replay::add_to_linker::<_, HasSelf<_>>(&mut linker, |state| { state @@ -191,3 +200,67 @@ impl WasiView for State { } } } + +mod dialog_bindings { + wasmtime::component::bindgen!({ + path: "assets/util.wit", + world: "host-dialog", + }); +} + +impl dialog_bindings::proxy::util::dialog::Host for crate::run::State { + fn print(&mut self, dep: u32, message: String) { + dialog::print(dep, &message); + } + fn read_string(&mut self, dep: u32) -> String { + dialog::read_string(dep) + } + fn read_u8(&mut self, dep: u32) -> String { + dialog::read_u8(dep) + } + fn read_u16(&mut self, dep: u32) -> String { + dialog::read_u16(dep) + } + fn read_u32(&mut self, dep: u32) -> String { + dialog::read_u32(dep) + } + fn read_u64(&mut self, dep: u32) -> String { + dialog::read_u64(dep) + } + fn read_s8(&mut self, dep: u32) -> String { + dialog::read_s8(dep) + } + fn read_s16(&mut self, dep: u32) -> String { + dialog::read_s16(dep) + } + fn read_s32(&mut self, dep: u32) -> String { + dialog::read_s32(dep) + } + fn read_s64(&mut self, dep: u32) -> String { + dialog::read_s64(dep) + } + fn read_f32(&mut self, dep: u32) -> String { + dialog::read_f32(dep) + } + fn read_f64(&mut self, dep: u32) -> String { + dialog::read_f64(dep) + } + fn read_bool(&mut self, dep: u32) -> String { + dialog::read_bool(dep) + } + fn read_char(&mut self, dep: u32) -> String { + dialog::read_char(dep) + } + fn read_select(&mut self, dep: u32, prompt: String, items: Vec) -> u32 { + dialog::read_select(dep, prompt, items) + } + fn read_multi_select(&mut self, dep: u32, prompt: String, items: Vec) -> Vec { + dialog::read_multi_select(dep, prompt, items) + } + fn read_num(&mut self, dep: u32, prompt: String) -> u32 { + dialog::read_num(dep, prompt) + } + fn read_raw_string(&mut self, dep: u32, prompt: String) -> String { + dialog::read_raw_string(dep, prompt) + } +} diff --git a/src/traits/dialog.rs b/src/traits/dialog.rs new file mode 100644 index 0000000..a619b72 --- /dev/null +++ b/src/traits/dialog.rs @@ -0,0 +1,280 @@ +use crate::traits::Trait; +use crate::util::make_path; +use heck::{ToKebabCase, ToSnakeCase}; +use quote::quote; +use syn::{Item, ItemEnum, ItemStruct, parse_quote}; + +pub struct DialogTrait; + +impl Trait for DialogTrait { + fn resource_trait(&self, module_path: &[String], resource: &ItemStruct) -> Vec { + let mut res = Vec::new(); + let resource_path = make_path(module_path, &resource.ident.to_string()); + let wit_name = resource.ident.to_string().to_kebab_case(); + let in_import = module_path[0] != "exports"; + if in_import { + let call = format!( + "get_mock_{}_magic42_{}", + module_path.join("_"), + resource.ident + ) + .to_snake_case(); + let call: syn::Ident = syn::parse_str(&call).unwrap(); + res.push(parse_quote! { + impl Dialog for #resource_path { + fn read_value(_dep: u32) -> Self { + let handle = HANDLE_ID.with(|id| { + let mut id = id.borrow_mut(); + let current_id = *id; + *id += 1; + current_id + }); + proxy::conversion::conversion::#call(handle) + } + } + }); + res.push(parse_quote! { + impl<'a> Dialog for &'a #resource_path { + fn read_value(_dep: u32) -> Self { + let handle = HANDLE_ID.with(|id| { + let mut id = id.borrow_mut(); + let current_id = *id; + *id += 1; + current_id + }); + SCOPED_ALLOC.with(|alloc| { + let mut alloc = alloc.borrow_mut(); + alloc.alloc(proxy::conversion::conversion::#call(handle)) + }) + } + } + }); + } else { + let borrow_path = make_path(module_path, &format!("{}Borrow<'a>", resource.ident)); + res.push(parse_quote! { + impl Dialog for #resource_path { + fn read_value(_dep: u32) -> Self { + let handle = HANDLE_ID.with(|id| { + let mut id = id.borrow_mut(); + let current_id = *id; + *id += 1; + current_id + }); + #resource_path::new(MockedResource { + handle, + name: #wit_name.to_string(), + }) + } + } + }); + res.push(parse_quote! { + impl<'a> Dialog for #borrow_path { + fn read_value(_dep: u32) -> Self { + unreachable!() + } + } + }); + } + res + } + fn struct_trait(&self, module_path: &[String], struct_item: &ItemStruct) -> Vec { + let mut res = Vec::new(); + let struct_name = make_path(module_path, &struct_item.ident.to_string()); + let (impl_generics, ty_generics, where_clause) = struct_item.generics.split_for_impl(); + let (field_names, tys) = match &struct_item.fields { + syn::Fields::Unit => (Vec::new(), Vec::new()), + syn::Fields::Named(fields) => { + let field_names: Vec<_> = fields + .named + .iter() + .map(|f| f.ident.clone().unwrap()) + .collect(); + let field_tys = fields.named.iter().map(|f| &f.ty).collect(); + (field_names, field_tys) + } + syn::Fields::Unnamed(_) => unreachable!(), + }; + res.push(parse_quote! { + impl #impl_generics Dialog for #struct_name #ty_generics #where_clause { + fn read_value(dep: u32) -> Self { + #( + proxy::util::dialog::print(dep + 1, &format!("provide value for field {}: {:60}", stringify!(#field_names), <#tys as ValueTyped>::value_type().to_string())); + let #field_names = Dialog::read_value(dep + 1); + )* + Self { + #( + #field_names, + )* + } + } + } + }); + res + } + fn enum_trait(&self, module_path: &[String], enum_item: &ItemEnum) -> Vec { + let mut res = Vec::new(); + let enum_name = make_path(module_path, &enum_item.ident.to_string()); + let (impl_generics, ty_generics, where_clause) = enum_item.generics.split_for_impl(); + let tags = enum_item.variants.iter().map(|variant| { + let tag = &variant.ident; + quote! { stringify!(#tag) } + }); + let tags = quote! { [#( #tags.to_string() ),*] }; + let arms = enum_item.variants.iter().enumerate().map(|(idx, variant)| { + let tag = &variant.ident; + match &variant.fields { + syn::Fields::Unit => quote! { + #idx => #enum_name::#tag + }, + syn::Fields::Unnamed(_) => quote! { + #idx => #enum_name::#tag(Dialog::read_value(dep + 1)) + }, + syn::Fields::Named(_) => unreachable!(), + } + }); + res.push(parse_quote! { + impl #impl_generics Dialog for #enum_name #ty_generics #where_clause { + fn read_value(dep: u32) -> Self { + let idx = proxy::util::dialog::read_select(dep, &format!("Select a variant for {}", stringify!(#enum_name)), &#tags) as usize; + match idx { + #( + #arms, + )* + _ => unreachable!(), + } + } + } + }); + res + } + fn flag_trait(&self, module_path: &[String], item: &crate::codegen::ItemFlag) -> Vec { + let mut res = Vec::new(); + let flag_path = make_path(module_path, &item.name.to_string()); + let flags = &item.flags; + let flag_names = quote! { [#( stringify!(#flags).to_string() ),*] }; + let flags = flags.iter().map(|flag| quote! { #flag_path::#flag }); + let idxs = 0..flags.len(); + res.push(parse_quote! { + impl Dialog for #flag_path { + fn read_value(dep: u32) -> Self { + let selections = proxy::util::dialog::read_multi_select(dep, &format!("Select flags for {}", stringify!(#flag_path)), &#flag_names); + let mut res = #flag_path::empty(); + for idx in selections { + match idx as usize { + #( + #idxs => res |= #flags, + )* + _ => unreachable!(), + } + } + res + } + } + }); + res + } + fn trait_defs(&self) -> Vec { + let ast: syn::File = parse_quote! { + trait Dialog { + fn read_value(dep: u32) -> Self; + } + thread_local! { + static HANDLE_ID: std::cell::RefCell = std::cell::RefCell::new(1); + } + impl Dialog for () { + fn read_value(_dep: u32) -> Self { + () + } + } + impl Dialog for Option { + fn read_value(dep: u32) -> Self { + let selection = proxy::util::dialog::read_select(dep, "Select None or Some", &["None".to_string(), "Some".to_string()]); + if selection == 0 { + None + } else { + Some(Dialog::read_value(dep + 1)) + } + } + } + impl Dialog for Vec { + fn read_value(dep: u32) -> Self { + use std::any::TypeId; + if TypeId::of::() == TypeId::of::() { + let hex = proxy::util::dialog::read_raw_string(dep, "Enter a string as list"); + let bytes = hex.into_bytes(); + unsafe { + std::mem::transmute::, Vec>(bytes) + } + } else { + let len = proxy::util::dialog::read_num(dep, "Enter the length of the list"); + (0..len).map(|_| Dialog::read_value(dep + 1)).collect() + } + } + } + impl Dialog for Result { + fn read_value(dep: u32) -> Self { + let selection = proxy::util::dialog::read_select(dep, "Select result", &["ok".to_string(), "err".to_string()]); + if selection == 0 { + Ok(Dialog::read_value(dep + 1)) + } else { + Err(Dialog::read_value(dep + 1)) + } + } + } + impl Dialog for MockedResource { + fn read_value(_dep: u32) -> Self { + Self { + handle: 42, + name: "mocked-resource".to_string(), + } + } + } + macro_rules! impl_dialog_primitive { + ($($ty:ty => $read_fn:ident),* $(,)?) => { + $( + impl Dialog for $ty { + fn read_value(dep: u32) -> Self { + let wave = proxy::util::dialog::$read_fn(dep); + let ret: Value = wasm_wave::from_str(&::value_type(), &wave).unwrap(); + ret.to_rust() + } + } + )* + }; + } + impl_dialog_primitive! { + bool => read_bool, + u8 => read_u8, + u16 => read_u16, + u32 => read_u32, + u64 => read_u64, + i8 => read_s8, + i16 => read_s16, + i32 => read_s32, + i64 => read_s64, + f32 => read_f32, + f64 => read_f64, + char => read_char, + String => read_string, + } + macro_rules! impl_dialog_tuple { + ($($T:ident),+) => { + impl<$($T: Dialog),+> Dialog for ($($T,)+) { + fn read_value(dep: u32) -> Self { + ($($T::read_value(dep + 1),)+) + } + } + }; + } + impl_dialog_tuple!(T1); + impl_dialog_tuple!(T1, T2); + impl_dialog_tuple!(T1, T2, T3); + impl_dialog_tuple!(T1, T2, T3, T4); + impl_dialog_tuple!(T1, T2, T3, T4, T5); + impl_dialog_tuple!(T1, T2, T3, T4, T5, T6); + impl_dialog_tuple!(T1, T2, T3, T4, T5, T6, T7); + impl_dialog_tuple!(T1, T2, T3, T4, T5, T6, T7, T8); + }; + ast.items + } +} diff --git a/src/traits/fuzz.rs b/src/traits/fuzz.rs index 8e4f392..5d3ff2a 100644 --- a/src/traits/fuzz.rs +++ b/src/traits/fuzz.rs @@ -142,6 +142,7 @@ impl Trait for FuzzTrait { _ => unreachable!(), } } + #[allow(unused_variables, unused_mut)] fn size_hint(depth: usize) -> (usize, Option) { let mut res = (1, Some(1)); #( @@ -168,7 +169,7 @@ impl Trait for FuzzTrait { let choices = [#(#flags),*]; for _ in 0..flag_count { let flag = u.choose(&choices)?; - res |= flag; + res |= *flag; } Ok(res) } @@ -183,6 +184,17 @@ impl Trait for FuzzTrait { let ast: syn::File = parse_quote! { #[allow(unused_imports)] use arbitrary::{Arbitrary, Unstructured, Result}; + impl Arbitrary<'_> for MockedResource { + fn arbitrary(_u: &mut Unstructured<'_>) -> Result { + Ok(Self { + handle: 42, + name: "mocked-resource".to_string(), + }) + } + fn size_hint(_: usize) -> (usize, Option) { + (0, Some(0)) + } + } }; ast.items } diff --git a/src/traits/mod.rs b/src/traits/mod.rs index 13d1c1c..06e8f44 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -1,6 +1,7 @@ use crate::codegen::{GenerateMode, ItemFlag, State, TypeInfo}; use syn::{Item, ItemEnum, ItemStruct}; +mod dialog; mod fuzz; mod proxy; mod wave; @@ -47,6 +48,14 @@ impl<'a> TraitGenerator<'a> { })); traits.push(Box::new(fuzz::FuzzTrait {})); } + GenerateMode::Dialog => { + traits.push(Box::new(wave::WaveTrait { + to_value: true, + to_rust: true, + has_replay_table: true, + })); + traits.push(Box::new(dialog::DialogTrait {})); + } } TraitGenerator { state, traits } }