From 71e37ccdb6795b764e6a81f880691b6bb463b3d2 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Fri, 27 Feb 2026 22:14:13 -0800 Subject: [PATCH 01/13] add dialog mode --- Cargo.toml | 2 +- assets/util.wit | 9 ++++ src/ast.rs | 18 +++++-- src/codegen/dialog.rs | 112 ++++++++++++++++++++++++++++++++++++++++++ src/codegen/mod.rs | 6 +++ src/instrument.rs | 6 ++- src/main.rs | 2 + src/run.rs | 21 +++++++- src/traits/mod.rs | 8 +++ 9 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 src/codegen/dialog.rs diff --git a/Cargo.toml b/Cargo.toml index c57f860..f6d790a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,4 +31,4 @@ wasmtime-wasi = { version = "42.0.0", optional = true } [features] run = ["wasmtime", "wasmtime-wasi"] -#default = ["run"] +default = ["run"] diff --git a/assets/util.wit b/assets/util.wit index 661dd04..44f874f 100644 --- a/assets/util.wit +++ b/assets/util.wit @@ -6,6 +6,15 @@ interface debug { get-random: func() -> list; } +interface dialog { + prompt: func(x: string); + input: func(x: string) -> string; +} + world crate-debug { export debug; } + +world host-dialog { + import dialog; +} \ No newline at end of file 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..1b567e1 --- /dev/null +++ b/src/codegen/dialog.rs @@ -0,0 +1,112 @@ +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 ret_ty.is_some() { + parse_quote! { + #sig { + let mut __params: Vec = Vec::new(); + #( + __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_names)).unwrap()); + )* + let mut __buf = __params.join(","); + proxy::util::dialog::prompt(&format!("import: {}({})", #display_name, __buf)); + __buf += #display_name; + let mut u = Unstructured::new(&__buf.as_bytes()); + let res = u.arbitrary().unwrap(); + let res_str = wasm_wave::to_string(&ToValue::to_value(&res)).unwrap(); + proxy::util::dialog::prompt(&format!("ret: {}", res_str)); + res + } + } + } else { + parse_quote! { + #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! { + { + let mut __params: Vec = Vec::new(); + #( + let #arg_name: #ty = u.arbitrary().unwrap(); + __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_name)).unwrap()); + )* + proxy::util::dialog::prompt(&format!("export: {}({})", #display_name, __params.join(", "))); + let _ = #func(#(#call_param),*); + } + }) + }) + }) + }) + .collect(); + let func_len = arms.iter().len(); + let idxs = 1..=func_len; + parse_quote! { + #sig { + let __buf = (0..4096).map(|i| i.to_string()).collect::>().join("").as_bytes().to_vec(); + let mut u = Unstructured::new(&__buf); + for _ in 0..10 { + let idx = u.int_in_range(1..=#func_len).unwrap(); + match idx { + #(#idxs => #arms)* + _ => unreachable!(), + } + // clean up borrowed resources from input args + SCOPED_ALLOC.with(|alloc| { + alloc.borrow_mut().clear(); + }); + } + } + } + } + } +} 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..df0a443 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, @@ -82,6 +82,9 @@ pub fn run(args: RunArgs) -> anyhow::Result<()> { logger: Logger::new(), exit_called: false, }; + dialog::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 +194,19 @@ impl WasiView for State { } } } + +mod dialog { + wasmtime::component::bindgen!({ + path: "assets/util.wit", + world: "host-dialog", + }); +} + +impl dialog::proxy::util::dialog::Host for crate::run::State { + fn input(&mut self, message: String) -> String { + message + } + fn prompt(&mut self, message: String) { + println!("{message}"); + } +} diff --git a/src/traits/mod.rs b/src/traits/mod.rs index 13d1c1c..90cb745 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -47,6 +47,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(fuzz::FuzzTrait {})); + } } TraitGenerator { state, traits } } From ce820f86f305020fe9b8fb62510982896c56f951 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Sat, 28 Feb 2026 20:00:41 -0800 Subject: [PATCH 02/13] dialog crate --- Cargo.lock | 57 +++++++++- Cargo.toml | 6 +- assets/util.wit | 5 +- crates/dialog/Cargo.toml | 9 ++ crates/dialog/src/lib.rs | 226 +++++++++++++++++++++++++++++++++++++++ src/codegen/dialog.rs | 6 +- src/run.rs | 22 ++-- src/traits/dialog.rs | 173 ++++++++++++++++++++++++++++++ src/traits/mod.rs | 2 + 9 files changed, 489 insertions(+), 17 deletions(-) create mode 100644 crates/dialog/Cargo.toml create mode 100644 crates/dialog/src/lib.rs create mode 100644 src/traits/dialog.rs diff --git a/Cargo.lock b/Cargo.lock index 06e6124..9405ae7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -307,6 +307,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" @@ -533,6 +546,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" @@ -593,6 +625,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" @@ -1332,6 +1370,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "dialog", "heck", "prettyplease", "proc-macro2", @@ -1628,6 +1667,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 +2138,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 +2231,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 f6d790a..f387413 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,8 @@ 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 } [features] -run = ["wasmtime", "wasmtime-wasi"] +run = ["wasmtime", "wasmtime-wasi", "dialog"] default = ["run"] diff --git a/assets/util.wit b/assets/util.wit index 44f874f..b5233cd 100644 --- a/assets/util.wit +++ b/assets/util.wit @@ -7,8 +7,9 @@ interface debug { } interface dialog { - prompt: func(x: string); - input: func(x: string) -> string; + print: func(x:string); + read-string: func(dep: u32) -> string; + read-bool: func(dep: u32) -> string; } world crate-debug { 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..ba313c2 --- /dev/null +++ b/crates/dialog/src/lib.rs @@ -0,0 +1,226 @@ +use console::{Style, StyledObject, style}; +use dialoguer::{Input, theme::Theme}; +use std::fmt; +use wasm_wave::value::convert::ToValue; + +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 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: String) { + let spaces = " ".repeat(self.indent * 2); + println!("{spaces}{prompt}"); + } + pub fn hint(&self, prompt: String) { + 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/codegen/dialog.rs b/src/codegen/dialog.rs index 1b567e1..e2c9908 100644 --- a/src/codegen/dialog.rs +++ b/src/codegen/dialog.rs @@ -28,12 +28,12 @@ impl State { __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_names)).unwrap()); )* let mut __buf = __params.join(","); - proxy::util::dialog::prompt(&format!("import: {}({})", #display_name, __buf)); + proxy::util::dialog::print(&format!("import: {}({})", #display_name, __buf)); __buf += #display_name; let mut u = Unstructured::new(&__buf.as_bytes()); let res = u.arbitrary().unwrap(); let res_str = wasm_wave::to_string(&ToValue::to_value(&res)).unwrap(); - proxy::util::dialog::prompt(&format!("ret: {}", res_str)); + proxy::util::dialog::print(&format!("ret: {}", res_str)); res } } @@ -80,7 +80,7 @@ impl State { let #arg_name: #ty = u.arbitrary().unwrap(); __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_name)).unwrap()); )* - proxy::util::dialog::prompt(&format!("export: {}({})", #display_name, __params.join(", "))); + proxy::util::dialog::print(&format!("export: {}({})", #display_name, __params.join(", "))); let _ = #func(#(#call_param),*); } }) diff --git a/src/run.rs b/src/run.rs index df0a443..69c4be6 100644 --- a/src/run.rs +++ b/src/run.rs @@ -82,9 +82,10 @@ pub fn run(args: RunArgs) -> anyhow::Result<()> { logger: Logger::new(), exit_called: false, }; - dialog::proxy::util::dialog::add_to_linker::>(&mut linker, |state| { - state - })?; + 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 @@ -195,18 +196,21 @@ impl WasiView for State { } } -mod dialog { +mod dialog_bindings { wasmtime::component::bindgen!({ path: "assets/util.wit", world: "host-dialog", }); } -impl dialog::proxy::util::dialog::Host for crate::run::State { - fn input(&mut self, message: String) -> String { - message +impl dialog_bindings::proxy::util::dialog::Host for crate::run::State { + fn print(&mut self, message: String) { + println!("{}", message); } - fn prompt(&mut self, message: String) { - println!("{message}"); + fn read_string(&mut self, dep: u32) -> String { + dialog::read_string(dep) + } + fn read_bool(&mut self, dep: u32) -> String { + todo!() } } diff --git a/src/traits/dialog.rs b/src/traits/dialog.rs new file mode 100644 index 0000000..60284d3 --- /dev/null +++ b/src/traits/dialog.rs @@ -0,0 +1,173 @@ +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 { + proxy::conversion::conversion::#call(42) + } + } + }); + res.push(parse_quote! { + impl<'a> Dialog for &'a #resource_path { + fn read_value(_dep: u32) -> Self { + SCOPED_ALLOC.with(|alloc| { + let mut alloc = alloc.borrow_mut(); + alloc.alloc(proxy::conversion::conversion::#call(42)) + }) + } + } + }); + } 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 { + Ok(#resource_path::new(MockedResource { + handle: 42, + 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 { + Self { + #( + #field_names: Dialog::read_value(0), + )* + } + } + } + }); + 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 arms = enum_item.variants.iter().enumerate().map(|(idx, variant)| { + let tag = &variant.ident; + match &variant.fields { + syn::Fields::Unit => quote! { + #idx => Ok(#enum_name::#tag) + }, + syn::Fields::Unnamed(_) => quote! { + #idx => Ok(#enum_name::#tag(u.arbitrary()?)) + }, + syn::Fields::Named(_) => unreachable!(), + } + }); + let size_hint = enum_item + .variants + .iter() + .map(|variant| match &variant.fields { + syn::Fields::Unit => quote! {}, + syn::Fields::Unnamed(f) => { + assert!(f.unnamed.len() == 1); + let ty = &f.unnamed.first().unwrap().ty; + quote! { + let size = <#ty as Arbitrary>::size_hint(depth + 1); + res = arbitrary::size_hint::or(res, size); + } + } + syn::Fields::Named(_) => unreachable!(), + }); + let variant_len = enum_item.variants.len(); + res.push(parse_quote! { + impl #impl_generics Dialog for #enum_name #ty_generics #where_clause { + fn read_value(dep: u32) -> Self { + todo!() + } + } + }); + 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 flag_num = item.flags.len() - 1; + let flags = item.flags.iter().map(|f| { + quote! { #flag_path::#f } + }); + res.push(parse_quote! { + impl Dialog for #flag_path { + fn read_value(_dep: u32) -> Self { + #( + #flags | + )* 0 + } + } + }); + res + } + fn trait_defs(&self) -> Vec { + let ast: syn::File = parse_quote! { + trait Dialog { + fn read_value(dep: u32) -> Self; + } + impl Dialog for String { + fn read_value(dep: u32) -> Self { + let wave = proxy::util::dialog::read_string(dep); + let ret: Value = wasm_wave::from_str(&::value_type(), &wave).unwrap(); + ret.to_rust() + } + } + impl Dialog for bool { + fn read_value(dep: u32) -> Self { + let wave = proxy::util::dialog::read_bool(dep); + let ret: Value = wasm_wave::from_str(&::value_type(), &wave).unwrap(); + ret.to_rust() + } + } + }; + ast.items + } +} diff --git a/src/traits/mod.rs b/src/traits/mod.rs index 90cb745..5cb58da 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; @@ -53,6 +54,7 @@ impl<'a> TraitGenerator<'a> { to_rust: true, has_replay_table: true, })); + traits.push(Box::new(dialog::DialogTrait {})); traits.push(Box::new(fuzz::FuzzTrait {})); } } From 6b1f713f8c7f92cd4de6f809be58e5447faa2557 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Sat, 28 Feb 2026 22:12:21 -0800 Subject: [PATCH 03/13] checkpoint --- assets/util.wit | 1 + crates/dialog/src/lib.rs | 17 +++++++++++++++-- src/codegen/dialog.rs | 6 +++++- src/run.rs | 5 ++++- src/traits/dialog.rs | 7 +++++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/assets/util.wit b/assets/util.wit index b5233cd..cc38eb8 100644 --- a/assets/util.wit +++ b/assets/util.wit @@ -9,6 +9,7 @@ interface debug { interface dialog { print: func(x:string); read-string: func(dep: u32) -> string; + read-u32: func(dep: u32) -> string; read-bool: func(dep: u32) -> string; } diff --git a/crates/dialog/src/lib.rs b/crates/dialog/src/lib.rs index ba313c2..c293415 100644 --- a/crates/dialog/src/lib.rs +++ b/crates/dialog/src/lib.rs @@ -3,6 +3,11 @@ use dialoguer::{Input, theme::Theme}; use std::fmt; use wasm_wave::value::convert::ToValue; +pub fn print(message: &str) { + let theme = IndentTheme::new(0); + theme.println(message); +} + pub fn read_string(dep: u32) -> String { let theme = IndentTheme::new(dep as usize); let text = Input::::with_theme(&theme) @@ -12,6 +17,14 @@ pub fn read_string(dep: u32) -> String { .unwrap(); wasm_wave::to_string(&text.to_value()).unwrap() } +pub fn read_u32(dep: u32) -> String { + let theme = IndentTheme::new(dep as usize); + let num = Input::::with_theme(&theme) + .with_prompt("Enter a u32") + .interact_text() + .unwrap(); + wasm_wave::to_string(&num.to_value()).unwrap() +} pub struct IndentTheme { indent: usize, @@ -54,11 +67,11 @@ impl IndentTheme { let spaces = " ".repeat(self.indent * 2); write!(f, "{spaces}") } - pub fn println(&self, prompt: String) { + pub fn println(&self, prompt: &str) { let spaces = " ".repeat(self.indent * 2); println!("{spaces}{prompt}"); } - pub fn hint(&self, prompt: String) { + pub fn hint(&self, prompt: &str) { let spaces = " ".repeat(self.indent * 2); println!("{spaces}{}", self.hint_style.apply_to(prompt)); } diff --git a/src/codegen/dialog.rs b/src/codegen/dialog.rs index e2c9908..90f169b 100644 --- a/src/codegen/dialog.rs +++ b/src/codegen/dialog.rs @@ -20,7 +20,7 @@ impl State { 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 ret_ty.is_some() { + if let Some(ty) = ret_ty { parse_quote! { #sig { let mut __params: Vec = Vec::new(); @@ -29,12 +29,16 @@ impl State { )* let mut __buf = __params.join(","); proxy::util::dialog::print(&format!("import: {}({})", #display_name, __buf)); + proxy::util::dialog::print(&format!("return type: {}", stringify!(#ty))); + #ty::read_value(0).to_value().to_rust() + /* __buf += #display_name; let mut u = Unstructured::new(&__buf.as_bytes()); let res = u.arbitrary().unwrap(); let res_str = wasm_wave::to_string(&ToValue::to_value(&res)).unwrap(); proxy::util::dialog::print(&format!("ret: {}", res_str)); res + */ } } } else { diff --git a/src/run.rs b/src/run.rs index 69c4be6..b4947c8 100644 --- a/src/run.rs +++ b/src/run.rs @@ -205,11 +205,14 @@ mod dialog_bindings { impl dialog_bindings::proxy::util::dialog::Host for crate::run::State { fn print(&mut self, message: String) { - println!("{}", message); + dialog::print(&message); } fn read_string(&mut self, dep: u32) -> String { dialog::read_string(dep) } + fn read_u32(&mut self, dep: u32) -> String { + dialog::read_u32(dep) + } fn read_bool(&mut self, dep: u32) -> String { todo!() } diff --git a/src/traits/dialog.rs b/src/traits/dialog.rs index 60284d3..76be6a5 100644 --- a/src/traits/dialog.rs +++ b/src/traits/dialog.rs @@ -160,6 +160,13 @@ impl Trait for DialogTrait { ret.to_rust() } } + impl Dialog for u32 { + fn read_value(dep: u32) -> Self { + let wave = proxy::util::dialog::read_u32(dep); + let ret: Value = wasm_wave::from_str(&::value_type(), &wave).unwrap(); + ret.to_rust() + } + } impl Dialog for bool { fn read_value(dep: u32) -> Self { let wave = proxy::util::dialog::read_bool(dep); From fef2b4d4aa06e9993df06edc12c2646603a96df0 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Sun, 1 Mar 2026 11:05:22 -0800 Subject: [PATCH 04/13] primitive types --- assets/util.wit | 12 ++++++- crates/dialog/src/lib.rs | 38 ++++++++++++++++---- src/run.rs | 32 ++++++++++++++++- src/traits/dialog.rs | 76 ++++++++++++++++++++++++---------------- 4 files changed, 119 insertions(+), 39 deletions(-) diff --git a/assets/util.wit b/assets/util.wit index cc38eb8..23aeed2 100644 --- a/assets/util.wit +++ b/assets/util.wit @@ -9,8 +9,18 @@ interface debug { interface dialog { print: func(x:string); read-string: func(dep: u32) -> string; - read-u32: func(dep: u32) -> string; read-bool: 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 { diff --git a/crates/dialog/src/lib.rs b/crates/dialog/src/lib.rs index c293415..40c1cdb 100644 --- a/crates/dialog/src/lib.rs +++ b/crates/dialog/src/lib.rs @@ -1,5 +1,5 @@ use console::{Style, StyledObject, style}; -use dialoguer::{Input, theme::Theme}; +use dialoguer::{Input, Select, theme::Theme}; use std::fmt; use wasm_wave::value::convert::ToValue; @@ -17,15 +17,41 @@ pub fn read_string(dep: u32) -> String { .unwrap(); wasm_wave::to_string(&text.to_value()).unwrap() } -pub fn read_u32(dep: u32) -> String { +pub fn read_bool(dep: u32) -> String { let theme = IndentTheme::new(dep as usize); - let num = Input::::with_theme(&theme) - .with_prompt("Enter a u32") - .interact_text() + let selection = Select::with_theme(&theme) + .with_prompt("Select a bool") + .items(&["true", "false"]) + .interact() .unwrap(); - wasm_wave::to_string(&num.to_value()).unwrap() + let value = if selection == 0 { true } else { false }; + wasm_wave::to_string(&value.to_value()).unwrap() +} +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, diff --git a/src/run.rs b/src/run.rs index b4947c8..93bca64 100644 --- a/src/run.rs +++ b/src/run.rs @@ -210,10 +210,40 @@ impl dialog_bindings::proxy::util::dialog::Host for crate::run::State { 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 { - todo!() + dialog::read_bool(dep) + } + fn read_char(&mut self, dep: u32) -> String { + dialog::read_char(dep) } } diff --git a/src/traits/dialog.rs b/src/traits/dialog.rs index 76be6a5..5eec23f 100644 --- a/src/traits/dialog.rs +++ b/src/traits/dialog.rs @@ -78,10 +78,10 @@ impl Trait for DialogTrait { }; res.push(parse_quote! { impl #impl_generics Dialog for #struct_name #ty_generics #where_clause { - fn read_value(_dep: u32) -> Self { + fn read_value(dep: u32) -> Self { Self { #( - #field_names: Dialog::read_value(0), + #field_names: Dialog::read_value(dep + 1), )* } } @@ -93,7 +93,7 @@ impl Trait for DialogTrait { 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 arms = enum_item.variants.iter().enumerate().map(|(idx, variant)| { + let _arms = enum_item.variants.iter().enumerate().map(|(idx, variant)| { let tag = &variant.ident; match &variant.fields { syn::Fields::Unit => quote! { @@ -105,7 +105,7 @@ impl Trait for DialogTrait { syn::Fields::Named(_) => unreachable!(), } }); - let size_hint = enum_item + let _size_hint = enum_item .variants .iter() .map(|variant| match &variant.fields { @@ -120,7 +120,7 @@ impl Trait for DialogTrait { } syn::Fields::Named(_) => unreachable!(), }); - let variant_len = enum_item.variants.len(); + let _variant_len = enum_item.variants.len(); res.push(parse_quote! { impl #impl_generics Dialog for #enum_name #ty_generics #where_clause { fn read_value(dep: u32) -> Self { @@ -133,16 +133,23 @@ impl Trait for DialogTrait { 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 flag_num = item.flags.len() - 1; - let flags = item.flags.iter().map(|f| { - quote! { #flag_path::#f } - }); + let _flag_num = item.flags.len() - 1; + let flags: Vec<_> = item + .flags + .iter() + .map(|f| { + quote! { #flag_path::#f } + }) + .collect(); + let flags_expr = if flags.is_empty() { + quote! { #flag_path::empty() } + } else { + quote! { #( #flags )|* } + }; res.push(parse_quote! { impl Dialog for #flag_path { fn read_value(_dep: u32) -> Self { - #( - #flags | - )* 0 + #flags_expr } } }); @@ -153,26 +160,33 @@ impl Trait for DialogTrait { trait Dialog { fn read_value(dep: u32) -> Self; } - impl Dialog for String { - fn read_value(dep: u32) -> Self { - let wave = proxy::util::dialog::read_string(dep); - let ret: Value = wasm_wave::from_str(&::value_type(), &wave).unwrap(); - ret.to_rust() - } - } - impl Dialog for u32 { - fn read_value(dep: u32) -> Self { - let wave = proxy::util::dialog::read_u32(dep); - let ret: Value = wasm_wave::from_str(&::value_type(), &wave).unwrap(); - ret.to_rust() - } + 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 for bool { - fn read_value(dep: u32) -> Self { - let wave = proxy::util::dialog::read_bool(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, } }; ast.items From 475fde2c252643182665bb5f9269d8b450133590 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Sun, 1 Mar 2026 12:48:25 -0800 Subject: [PATCH 05/13] enum --- Cargo.lock | 66 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + assets/util.wit | 3 +- crates/dialog/src/lib.rs | 11 +++++++ src/codegen/dialog.rs | 4 ++- src/run.rs | 8 +++++ src/traits/dialog.rs | 35 +++++++++------------ 7 files changed, 106 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9405ae7..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" @@ -529,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" @@ -596,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" @@ -1263,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" @@ -1370,6 +1435,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", + "ctrlc", "dialog", "heck", "prettyplease", diff --git a/Cargo.toml b/Cargo.toml index f387413..03056ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ 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", "dialog"] diff --git a/assets/util.wit b/assets/util.wit index 23aeed2..7e4e59f 100644 --- a/assets/util.wit +++ b/assets/util.wit @@ -8,8 +8,9 @@ interface debug { interface dialog { print: func(x:string); - read-string: func(dep: u32) -> string; + read-selection: func(dep: u32, prompt: string, items: list) -> u32; 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; diff --git a/crates/dialog/src/lib.rs b/crates/dialog/src/lib.rs index 40c1cdb..e501489 100644 --- a/crates/dialog/src/lib.rs +++ b/crates/dialog/src/lib.rs @@ -3,6 +3,8 @@ use dialoguer::{Input, Select, theme::Theme}; use std::fmt; use wasm_wave::value::convert::ToValue; +pub use console; + pub fn print(message: &str) { let theme = IndentTheme::new(0); theme.println(message); @@ -27,6 +29,15 @@ pub fn read_bool(dep: u32) -> String { let value = if selection == 0 { true } else { false }; wasm_wave::to_string(&value.to_value()).unwrap() } +pub fn read_selection(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) + .interact() + .unwrap(); + selection as u32 +} macro_rules! read_primitive { ($fn_name:ident, $ty:ty, $prompt:expr) => { pub fn $fn_name(dep: u32) -> String { diff --git a/src/codegen/dialog.rs b/src/codegen/dialog.rs index 90f169b..6f3e4cf 100644 --- a/src/codegen/dialog.rs +++ b/src/codegen/dialog.rs @@ -79,9 +79,11 @@ impl State { let display_name = wit_func_name(path, resource, &sig.ident, &kind); Some(quote! { { + proxy::util::dialog::print(&format!("call export func {}", #display_name)); let mut __params: Vec = Vec::new(); #( - let #arg_name: #ty = u.arbitrary().unwrap(); + proxy::util::dialog::print(&format!("provide argument for {}", stringify!(#arg_name))); + let #arg_name = #ty::read_value(0); __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_name)).unwrap()); )* proxy::util::dialog::print(&format!("export: {}({})", #display_name, __params.join(", "))); diff --git a/src/run.rs b/src/run.rs index 93bca64..812e41d 100644 --- a/src/run.rs +++ b/src/run.rs @@ -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 untill 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) @@ -246,4 +251,7 @@ impl dialog_bindings::proxy::util::dialog::Host for crate::run::State { fn read_char(&mut self, dep: u32) -> String { dialog::read_char(dep) } + fn read_selection(&mut self, dep: u32, prompt: String, items: Vec) -> u32 { + dialog::read_selection(dep, prompt, items) + } } diff --git a/src/traits/dialog.rs b/src/traits/dialog.rs index 5eec23f..8083163 100644 --- a/src/traits/dialog.rs +++ b/src/traits/dialog.rs @@ -93,38 +93,33 @@ impl Trait for DialogTrait { 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 _arms = enum_item.variants.iter().enumerate().map(|(idx, variant)| { + 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 => Ok(#enum_name::#tag) + #idx => #enum_name::#tag }, syn::Fields::Unnamed(_) => quote! { - #idx => Ok(#enum_name::#tag(u.arbitrary()?)) + #idx => #enum_name::#tag(Dialog::read_value(dep + 1)) }, syn::Fields::Named(_) => unreachable!(), } }); - let _size_hint = enum_item - .variants - .iter() - .map(|variant| match &variant.fields { - syn::Fields::Unit => quote! {}, - syn::Fields::Unnamed(f) => { - assert!(f.unnamed.len() == 1); - let ty = &f.unnamed.first().unwrap().ty; - quote! { - let size = <#ty as Arbitrary>::size_hint(depth + 1); - res = arbitrary::size_hint::or(res, size); - } - } - syn::Fields::Named(_) => unreachable!(), - }); - let _variant_len = enum_item.variants.len(); res.push(parse_quote! { impl #impl_generics Dialog for #enum_name #ty_generics #where_clause { fn read_value(dep: u32) -> Self { - todo!() + let idx = proxy::util::dialog::read_selection(dep, &format!("Select a variant for {}", stringify!(#enum_name)), &#tags) as usize; + match idx { + #( + #arms, + )* + _ => unreachable!(), + } } } }); From 451e030f6ae612f0cc13d15bcada03eec083c059 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Sun, 1 Mar 2026 16:51:43 -0800 Subject: [PATCH 06/13] rust works --- assets/util.wit | 1 + crates/dialog/src/lib.rs | 9 ++++++ src/codegen/dialog.rs | 15 ++-------- src/run.rs | 5 +++- src/traits/dialog.rs | 62 ++++++++++++++++++++++++++++++++++++++-- 5 files changed, 76 insertions(+), 16 deletions(-) diff --git a/assets/util.wit b/assets/util.wit index 7e4e59f..5105328 100644 --- a/assets/util.wit +++ b/assets/util.wit @@ -8,6 +8,7 @@ interface debug { interface dialog { print: func(x:string); + read-num: func(dep: u32, prompt: string) -> u32; read-selection: func(dep: u32, prompt: string, items: list) -> u32; read-bool: func(dep: u32) -> string; read-string: func(dep: u32) -> string; diff --git a/crates/dialog/src/lib.rs b/crates/dialog/src/lib.rs index e501489..9ee25b7 100644 --- a/crates/dialog/src/lib.rs +++ b/crates/dialog/src/lib.rs @@ -29,11 +29,20 @@ pub fn read_bool(dep: u32) -> String { 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_selection(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 diff --git a/src/codegen/dialog.rs b/src/codegen/dialog.rs index 6f3e4cf..257f237 100644 --- a/src/codegen/dialog.rs +++ b/src/codegen/dialog.rs @@ -27,18 +27,9 @@ impl State { #( __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_names)).unwrap()); )* - let mut __buf = __params.join(","); - proxy::util::dialog::print(&format!("import: {}({})", #display_name, __buf)); + proxy::util::dialog::print(&format!("import: {}({})", #display_name, __params.join(", "))); proxy::util::dialog::print(&format!("return type: {}", stringify!(#ty))); - #ty::read_value(0).to_value().to_rust() - /* - __buf += #display_name; - let mut u = Unstructured::new(&__buf.as_bytes()); - let res = u.arbitrary().unwrap(); - let res_str = wasm_wave::to_string(&ToValue::to_value(&res)).unwrap(); - proxy::util::dialog::print(&format!("ret: {}", res_str)); - res - */ + Dialog::read_value(0) } } } else { @@ -83,7 +74,7 @@ impl State { let mut __params: Vec = Vec::new(); #( proxy::util::dialog::print(&format!("provide argument for {}", stringify!(#arg_name))); - let #arg_name = #ty::read_value(0); + let #arg_name: #ty = Dialog::read_value(0); __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_name)).unwrap()); )* proxy::util::dialog::print(&format!("export: {}({})", #display_name, __params.join(", "))); diff --git a/src/run.rs b/src/run.rs index 812e41d..70be0d3 100644 --- a/src/run.rs +++ b/src/run.rs @@ -66,7 +66,7 @@ impl bindings::proxy::recorder::replay::Host for State { const MAX_FUEL: u64 = u64::MAX; pub fn run(args: RunArgs) -> anyhow::Result<()> { - // Patch ctrlc untill https://github.com/console-rs/dialoguer/issues/77 is fixed + // 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(); @@ -254,4 +254,7 @@ impl dialog_bindings::proxy::util::dialog::Host for crate::run::State { fn read_selection(&mut self, dep: u32, prompt: String, items: Vec) -> u32 { dialog::read_selection(dep, prompt, items) } + fn read_num(&mut self, dep: u32, prompt: String) -> u32 { + dialog::read_num(dep, prompt) + } } diff --git a/src/traits/dialog.rs b/src/traits/dialog.rs index 8083163..bf72002 100644 --- a/src/traits/dialog.rs +++ b/src/traits/dialog.rs @@ -42,10 +42,10 @@ impl Trait for DialogTrait { res.push(parse_quote! { impl Dialog for #resource_path { fn read_value(_dep: u32) -> Self { - Ok(#resource_path::new(MockedResource { + #resource_path::new(MockedResource { handle: 42, name: #wit_name.to_string(), - })) + }) } } }); @@ -128,7 +128,6 @@ impl Trait for DialogTrait { 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 _flag_num = item.flags.len() - 1; let flags: Vec<_> = item .flags .iter() @@ -155,6 +154,46 @@ impl Trait for DialogTrait { trait Dialog { fn read_value(dep: u32) -> Self; } + 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_selection(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_string(dep); + 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_selection(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)) + } + } + } macro_rules! impl_dialog_primitive { ($($ty:ty => $read_fn:ident),* $(,)?) => { $( @@ -183,6 +222,23 @@ impl Trait for DialogTrait { 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 } From 46cf301b0c4f852e34d5b362717b2e81cc65fa4b Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Sun, 1 Mar 2026 19:24:15 -0800 Subject: [PATCH 07/13] fix --- assets/util.wit | 2 +- crates/dialog/src/lib.rs | 2 +- src/codegen/dialog.rs | 46 ++++++++++++++++++++++++---------------- src/codegen/fuzz.rs | 8 ++++++- src/run.rs | 4 ++-- src/traits/dialog.rs | 24 +++++++++------------ src/traits/fuzz.rs | 1 + 7 files changed, 50 insertions(+), 37 deletions(-) diff --git a/assets/util.wit b/assets/util.wit index 5105328..acbe252 100644 --- a/assets/util.wit +++ b/assets/util.wit @@ -9,7 +9,7 @@ interface debug { interface dialog { print: func(x:string); read-num: func(dep: u32, prompt: string) -> u32; - read-selection: func(dep: u32, prompt: string, items: list) -> u32; + read-select: func(dep: u32, prompt: string, items: list) -> u32; read-bool: func(dep: u32) -> string; read-string: func(dep: u32) -> string; read-u8: func(dep: u32) -> string; diff --git a/crates/dialog/src/lib.rs b/crates/dialog/src/lib.rs index 9ee25b7..a9442bf 100644 --- a/crates/dialog/src/lib.rs +++ b/crates/dialog/src/lib.rs @@ -37,7 +37,7 @@ pub fn read_num(dep: u32, prompt: String) -> u32 { .unwrap(); num } -pub fn read_selection(dep: u32, prompt: String, items: Vec) -> u32 { +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) diff --git a/src/codegen/dialog.rs b/src/codegen/dialog.rs index 257f237..e8a0b9e 100644 --- a/src/codegen/dialog.rs +++ b/src/codegen/dialog.rs @@ -21,19 +21,27 @@ impl State { 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 = Vec::new(); + let mut __params: Vec = #init_vec; #( __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_names)).unwrap()); )* proxy::util::dialog::print(&format!("import: {}({})", #display_name, __params.join(", "))); proxy::util::dialog::print(&format!("return type: {}", stringify!(#ty))); - Dialog::read_value(0) + let ret = Dialog::read_value(0); + proxy::util::dialog::print(&format!("ret: {}", wasm_wave::to_string(&ToValue::to_value(&ret)).unwrap())); + ret } } } else { parse_quote! { + #[allow(unused_variables)] #sig {} } } @@ -68,7 +76,7 @@ impl State { }; let func = make_path(path, &func_name); let display_name = wit_func_name(path, resource, &sig.ident, &kind); - Some(quote! { + Some((quote! { { proxy::util::dialog::print(&format!("call export func {}", #display_name)); let mut __params: Vec = Vec::new(); @@ -80,29 +88,31 @@ impl State { proxy::util::dialog::print(&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 { - let __buf = (0..4096).map(|i| i.to_string()).collect::>().join("").as_bytes().to_vec(); - let mut u = Unstructured::new(&__buf); - for _ in 0..10 { - let idx = u.int_in_range(1..=#func_len).unwrap(); - match idx { - #(#idxs => #arms)* - _ => unreachable!(), - } - // clean up borrowed resources from input args - SCOPED_ALLOC.with(|alloc| { - alloc.borrow_mut().clear(); - }); - } + #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/run.rs b/src/run.rs index 70be0d3..3c1ea26 100644 --- a/src/run.rs +++ b/src/run.rs @@ -251,8 +251,8 @@ impl dialog_bindings::proxy::util::dialog::Host for crate::run::State { fn read_char(&mut self, dep: u32) -> String { dialog::read_char(dep) } - fn read_selection(&mut self, dep: u32, prompt: String, items: Vec) -> u32 { - dialog::read_selection(dep, prompt, items) + fn read_select(&mut self, dep: u32, prompt: String, items: Vec) -> u32 { + dialog::read_select(dep, prompt, items) } fn read_num(&mut self, dep: u32, prompt: String) -> u32 { dialog::read_num(dep, prompt) diff --git a/src/traits/dialog.rs b/src/traits/dialog.rs index bf72002..375995e 100644 --- a/src/traits/dialog.rs +++ b/src/traits/dialog.rs @@ -63,17 +63,13 @@ impl Trait for DialogTrait { 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) - } + let field_names = match &struct_item.fields { + syn::Fields::Unit => Vec::new(), + syn::Fields::Named(fields) => fields + .named + .iter() + .map(|f| f.ident.clone().unwrap()) + .collect(), syn::Fields::Unnamed(_) => unreachable!(), }; res.push(parse_quote! { @@ -113,7 +109,7 @@ impl Trait for DialogTrait { 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_selection(dep, &format!("Select a variant for {}", stringify!(#enum_name)), &#tags) as usize; + let idx = proxy::util::dialog::read_select(dep, &format!("Select a variant for {}", stringify!(#enum_name)), &#tags) as usize; match idx { #( #arms, @@ -161,7 +157,7 @@ impl Trait for DialogTrait { } impl Dialog for Option { fn read_value(dep: u32) -> Self { - let selection = proxy::util::dialog::read_selection(dep, "Select None or Some", &["None".to_string(), "Some".to_string()]); + let selection = proxy::util::dialog::read_select(dep, "Select None or Some", &["None".to_string(), "Some".to_string()]); if selection == 0 { None } else { @@ -186,7 +182,7 @@ impl Trait for DialogTrait { } impl Dialog for Result { fn read_value(dep: u32) -> Self { - let selection = proxy::util::dialog::read_selection(dep, "Select result", &["ok".to_string(), "err".to_string()]); + 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 { diff --git a/src/traits/fuzz.rs b/src/traits/fuzz.rs index 8e4f392..22e6889 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)); #( From 9fbde4231f61cb5856f696c4beecf0180cbb1e71 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Sun, 1 Mar 2026 20:19:11 -0800 Subject: [PATCH 08/13] flags --- assets/util.wit | 1 + crates/dialog/src/lib.rs | 9 ++++++++ src/run.rs | 3 +++ src/traits/dialog.rs | 44 ++++++++++++++++++++++++++-------------- src/traits/fuzz.rs | 2 +- 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/assets/util.wit b/assets/util.wit index acbe252..dadeb7b 100644 --- a/assets/util.wit +++ b/assets/util.wit @@ -10,6 +10,7 @@ interface dialog { print: func(x: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; diff --git a/crates/dialog/src/lib.rs b/crates/dialog/src/lib.rs index a9442bf..2270c5e 100644 --- a/crates/dialog/src/lib.rs +++ b/crates/dialog/src/lib.rs @@ -47,6 +47,15 @@ pub fn read_select(dep: u32, prompt: String, items: Vec) -> u32 { .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 { diff --git a/src/run.rs b/src/run.rs index 3c1ea26..4fbaf39 100644 --- a/src/run.rs +++ b/src/run.rs @@ -254,6 +254,9 @@ impl dialog_bindings::proxy::util::dialog::Host for crate::run::State { 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) } diff --git a/src/traits/dialog.rs b/src/traits/dialog.rs index 375995e..31a5ba0 100644 --- a/src/traits/dialog.rs +++ b/src/traits/dialog.rs @@ -75,9 +75,13 @@ impl Trait for DialogTrait { 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(&format!("provide value for field {}", stringify!(#field_names))); + let #field_names = Dialog::read_value(dep + 1); + )* Self { #( - #field_names: Dialog::read_value(dep + 1), + #field_names, )* } } @@ -124,22 +128,24 @@ impl Trait for DialogTrait { 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: Vec<_> = item - .flags - .iter() - .map(|f| { - quote! { #flag_path::#f } - }) - .collect(); - let flags_expr = if flags.is_empty() { - quote! { #flag_path::empty() } - } else { - quote! { #( #flags )|* } - }; + 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 { - #flags_expr + 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 } } }); @@ -190,6 +196,14 @@ impl Trait for DialogTrait { } } } + 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),* $(,)?) => { $( diff --git a/src/traits/fuzz.rs b/src/traits/fuzz.rs index 22e6889..8b5017b 100644 --- a/src/traits/fuzz.rs +++ b/src/traits/fuzz.rs @@ -169,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) } From 5127c32b7e4d1960f85005616eb801dcb12c23a3 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Sun, 1 Mar 2026 21:33:10 -0800 Subject: [PATCH 09/13] makefile --- Makefile | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) 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 \ From 4169fcb159a39e5b91707368e31780d16d780ff1 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Sun, 1 Mar 2026 21:47:31 -0800 Subject: [PATCH 10/13] fix --- src/traits/fuzz.rs | 11 +++++++++++ src/traits/mod.rs | 1 - 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/traits/fuzz.rs b/src/traits/fuzz.rs index 8b5017b..5d3ff2a 100644 --- a/src/traits/fuzz.rs +++ b/src/traits/fuzz.rs @@ -184,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 5cb58da..06e8f44 100644 --- a/src/traits/mod.rs +++ b/src/traits/mod.rs @@ -55,7 +55,6 @@ impl<'a> TraitGenerator<'a> { has_replay_table: true, })); traits.push(Box::new(dialog::DialogTrait {})); - traits.push(Box::new(fuzz::FuzzTrait {})); } } TraitGenerator { state, traits } From 2ef7754b558bb8f7d9c541be23934f61f76963d2 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Mon, 2 Mar 2026 13:20:04 -0800 Subject: [PATCH 11/13] fix resource name; add raw_string --- assets/util.wit | 1 + crates/dialog/src/lib.rs | 9 +++++++++ src/codegen/dialog.rs | 2 +- src/run.rs | 3 +++ src/traits/dialog.rs | 29 +++++++++++++++++++++++++---- 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/assets/util.wit b/assets/util.wit index dadeb7b..4417c8c 100644 --- a/assets/util.wit +++ b/assets/util.wit @@ -8,6 +8,7 @@ interface debug { interface dialog { print: func(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; diff --git a/crates/dialog/src/lib.rs b/crates/dialog/src/lib.rs index 2270c5e..37106b7 100644 --- a/crates/dialog/src/lib.rs +++ b/crates/dialog/src/lib.rs @@ -37,6 +37,15 @@ pub fn read_num(dep: u32, prompt: String) -> u32 { .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) diff --git a/src/codegen/dialog.rs b/src/codegen/dialog.rs index e8a0b9e..0ea6016 100644 --- a/src/codegen/dialog.rs +++ b/src/codegen/dialog.rs @@ -33,7 +33,7 @@ impl State { __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_names)).unwrap()); )* proxy::util::dialog::print(&format!("import: {}({})", #display_name, __params.join(", "))); - proxy::util::dialog::print(&format!("return type: {}", stringify!(#ty))); + proxy::util::dialog::print(&format!("return type: {:.60}", <#ty as ValueTyped>::value_type().to_string())); let ret = Dialog::read_value(0); proxy::util::dialog::print(&format!("ret: {}", wasm_wave::to_string(&ToValue::to_value(&ret)).unwrap())); ret diff --git a/src/run.rs b/src/run.rs index 4fbaf39..1a054e8 100644 --- a/src/run.rs +++ b/src/run.rs @@ -260,4 +260,7 @@ impl dialog_bindings::proxy::util::dialog::Host for crate::run::State { 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 index 31a5ba0..bb95025 100644 --- a/src/traits/dialog.rs +++ b/src/traits/dialog.rs @@ -23,16 +23,28 @@ impl Trait for DialogTrait { res.push(parse_quote! { impl Dialog for #resource_path { fn read_value(_dep: u32) -> Self { - proxy::conversion::conversion::#call(42) + 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(42)) + alloc.alloc(proxy::conversion::conversion::#call(handle)) }) } } @@ -42,8 +54,14 @@ impl Trait for DialogTrait { 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: 42, + handle, name: #wit_name.to_string(), }) } @@ -156,6 +174,9 @@ impl Trait for DialogTrait { 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 { () @@ -175,7 +196,7 @@ impl Trait for DialogTrait { fn read_value(dep: u32) -> Self { use std::any::TypeId; if TypeId::of::() == TypeId::of::() { - let hex = proxy::util::dialog::read_string(dep); + 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) From 5262c4e2fd5f14c54b3e60d271447403234fd3bb Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Mon, 2 Mar 2026 13:50:42 -0800 Subject: [PATCH 12/13] fix --- assets/util.wit | 2 +- crates/dialog/src/lib.rs | 4 ++-- src/codegen/dialog.rs | 12 ++++++------ src/run.rs | 4 ++-- src/traits/dialog.rs | 20 ++++++++++++-------- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/assets/util.wit b/assets/util.wit index 4417c8c..5e9ca6d 100644 --- a/assets/util.wit +++ b/assets/util.wit @@ -7,7 +7,7 @@ interface debug { } interface dialog { - print: func(x:string); + 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; diff --git a/crates/dialog/src/lib.rs b/crates/dialog/src/lib.rs index 37106b7..6bc580a 100644 --- a/crates/dialog/src/lib.rs +++ b/crates/dialog/src/lib.rs @@ -5,8 +5,8 @@ use wasm_wave::value::convert::ToValue; pub use console; -pub fn print(message: &str) { - let theme = IndentTheme::new(0); +pub fn print(dep: u32, message: &str) { + let theme = IndentTheme::new(dep as usize); theme.println(message); } diff --git a/src/codegen/dialog.rs b/src/codegen/dialog.rs index 0ea6016..ec56b23 100644 --- a/src/codegen/dialog.rs +++ b/src/codegen/dialog.rs @@ -32,10 +32,10 @@ impl State { #( __params.push(wasm_wave::to_string(&ToValue::to_value(&#arg_names)).unwrap()); )* - proxy::util::dialog::print(&format!("import: {}({})", #display_name, __params.join(", "))); - proxy::util::dialog::print(&format!("return type: {:.60}", <#ty as ValueTyped>::value_type().to_string())); + 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(&format!("ret: {}", wasm_wave::to_string(&ToValue::to_value(&ret)).unwrap())); + proxy::util::dialog::print(0, &format!("ret: {}", wasm_wave::to_string(&ToValue::to_value(&ret)).unwrap())); ret } } @@ -78,14 +78,14 @@ impl State { let display_name = wit_func_name(path, resource, &sig.ident, &kind); Some((quote! { { - proxy::util::dialog::print(&format!("call export func {}", #display_name)); + proxy::util::dialog::print(0, &format!("call export func {}", #display_name)); let mut __params: Vec = Vec::new(); #( - proxy::util::dialog::print(&format!("provide argument for {}", stringify!(#arg_name))); + 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(&format!("export: {}({})", #display_name, __params.join(", "))); + proxy::util::dialog::print(0, &format!("export: {}({})", #display_name, __params.join(", "))); let _ = #func(#(#call_param),*); } }, display_name)) diff --git a/src/run.rs b/src/run.rs index 1a054e8..b71dc7c 100644 --- a/src/run.rs +++ b/src/run.rs @@ -209,8 +209,8 @@ mod dialog_bindings { } impl dialog_bindings::proxy::util::dialog::Host for crate::run::State { - fn print(&mut self, message: String) { - dialog::print(&message); + fn print(&mut self, dep: u32, message: String) { + dialog::print(dep, &message); } fn read_string(&mut self, dep: u32) -> String { dialog::read_string(dep) diff --git a/src/traits/dialog.rs b/src/traits/dialog.rs index bb95025..a619b72 100644 --- a/src/traits/dialog.rs +++ b/src/traits/dialog.rs @@ -81,20 +81,24 @@ impl Trait for DialogTrait { 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 = match &struct_item.fields { - syn::Fields::Unit => Vec::new(), - syn::Fields::Named(fields) => fields - .named - .iter() - .map(|f| f.ident.clone().unwrap()) - .collect(), + 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(&format!("provide value for field {}", stringify!(#field_names))); + 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 { From 1defa4b62bab9a2a39577b3c7f4bf61794f6e3a8 Mon Sep 17 00:00:00 2001 From: Yan Chen Date: Mon, 2 Mar 2026 14:14:26 -0800 Subject: [PATCH 13/13] fix --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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