1 //! Support for parsing Wasmtime's `-O`, `-W`, etc "option groups" 2 //! 3 //! This builds up a clap-derive-like system where there's ideally a single 4 //! macro `wasmtime_option_group!` which is invoked per-option which enables 5 //! specifying options in a struct-like syntax where all other boilerplate about 6 //! option parsing is contained exclusively within this module. 7 8 use crate::{KeyValuePair, WasiNnGraph}; 9 use anyhow::{bail, Result}; 10 use clap::builder::{StringValueParser, TypedValueParser, ValueParserFactory}; 11 use clap::error::{Error, ErrorKind}; 12 use std::marker; 13 use std::time::Duration; 14 15 /// Characters which can be safely ignored while parsing numeric options to wasmtime 16 const IGNORED_NUMBER_CHARS: [char; 1] = ['_']; 17 18 #[macro_export] 19 macro_rules! wasmtime_option_group { 20 ( 21 $(#[$attr:meta])* 22 pub struct $opts:ident { 23 $( 24 $(#[doc = $doc:tt])* 25 pub $opt:ident: $container:ident<$payload:ty>, 26 )+ 27 28 $( 29 #[prefixed = $prefix:tt] 30 $(#[doc = $prefixed_doc:tt])* 31 pub $prefixed:ident: Vec<(String, Option<String>)>, 32 )? 33 } 34 enum $option:ident { 35 ... 36 } 37 ) => { 38 #[derive(Default, Debug)] 39 $(#[$attr])* 40 pub struct $opts { 41 $( 42 pub $opt: $container<$payload>, 43 )+ 44 $( 45 pub $prefixed: Vec<(String, Option<String>)>, 46 )? 47 } 48 49 #[derive(Clone, Debug,PartialEq)] 50 #[expect(non_camel_case_types, reason = "macro-generated code")] 51 enum $option { 52 $( 53 $opt($payload), 54 )+ 55 $( 56 $prefixed(String, Option<String>), 57 )? 58 } 59 60 impl $crate::opt::WasmtimeOption for $option { 61 const OPTIONS: &'static [$crate::opt::OptionDesc<$option>] = &[ 62 $( 63 $crate::opt::OptionDesc { 64 name: $crate::opt::OptName::Name(stringify!($opt)), 65 parse: |_, s| { 66 Ok($option::$opt( 67 $crate::opt::WasmtimeOptionValue::parse(s)? 68 )) 69 }, 70 val_help: <$payload as $crate::opt::WasmtimeOptionValue>::VAL_HELP, 71 docs: concat!($($doc, "\n",)*), 72 }, 73 )+ 74 $( 75 $crate::opt::OptionDesc { 76 name: $crate::opt::OptName::Prefix($prefix), 77 parse: |name, val| { 78 Ok($option::$prefixed( 79 name.to_string(), 80 val.map(|v| v.to_string()), 81 )) 82 }, 83 val_help: "[=val]", 84 docs: concat!($($prefixed_doc, "\n",)*), 85 }, 86 )? 87 ]; 88 } 89 90 impl $opts { 91 fn configure_with(&mut self, opts: &[$crate::opt::CommaSeparated<$option>]) { 92 for opt in opts.iter().flat_map(|o| o.0.iter()) { 93 match opt { 94 $( 95 $option::$opt(val) => { 96 let dst = &mut self.$opt; 97 wasmtime_option_group!(@push $container dst val); 98 } 99 )+ 100 $( 101 $option::$prefixed(key, val) => self.$prefixed.push((key.clone(), val.clone())), 102 )? 103 } 104 } 105 } 106 } 107 }; 108 109 (@push Option $dst:ident $val:ident) => (*$dst = Some($val.clone())); 110 (@push Vec $dst:ident $val:ident) => ($dst.push($val.clone())); 111 } 112 113 /// Parser registered with clap which handles parsing the `...` in `-O ...`. 114 #[derive(Clone, Debug, PartialEq)] 115 pub struct CommaSeparated<T>(pub Vec<T>); 116 117 impl<T> ValueParserFactory for CommaSeparated<T> 118 where 119 T: WasmtimeOption, 120 { 121 type Parser = CommaSeparatedParser<T>; 122 123 fn value_parser() -> CommaSeparatedParser<T> { 124 CommaSeparatedParser(marker::PhantomData) 125 } 126 } 127 128 #[derive(Clone)] 129 pub struct CommaSeparatedParser<T>(marker::PhantomData<T>); 130 131 impl<T> TypedValueParser for CommaSeparatedParser<T> 132 where 133 T: WasmtimeOption, 134 { 135 type Value = CommaSeparated<T>; 136 137 fn parse_ref( 138 &self, 139 cmd: &clap::Command, 140 arg: Option<&clap::Arg>, 141 value: &std::ffi::OsStr, 142 ) -> Result<Self::Value, Error> { 143 let val = StringValueParser::new().parse_ref(cmd, arg, value)?; 144 145 let options = T::OPTIONS; 146 let arg = arg.expect("should always have an argument"); 147 let arg_long = arg.get_long().expect("should have a long name specified"); 148 let arg_short = arg.get_short().expect("should have a short name specified"); 149 150 // Handle `-O help` which dumps all the `-O` options, their messages, 151 // and then exits. 152 if val == "help" { 153 let mut max = 0; 154 for d in options { 155 max = max.max(d.name.display_string().len() + d.val_help.len()); 156 } 157 println!("Available {arg_long} options:\n"); 158 for d in options { 159 print!( 160 " -{arg_short} {:>1$}", 161 d.name.display_string(), 162 max - d.val_help.len() 163 ); 164 print!("{}", d.val_help); 165 print!(" --"); 166 if val == "help" { 167 for line in d.docs.lines().map(|s| s.trim()) { 168 if line.is_empty() { 169 break; 170 } 171 print!(" {line}"); 172 } 173 println!(); 174 } else { 175 println!(); 176 for line in d.docs.lines().map(|s| s.trim()) { 177 let line = line.trim(); 178 println!(" {line}"); 179 } 180 } 181 } 182 println!("\npass `-{arg_short} help-long` to see longer-form explanations"); 183 std::process::exit(0); 184 } 185 if val == "help-long" { 186 println!("Available {arg_long} options:\n"); 187 for d in options { 188 println!( 189 " -{arg_short} {}{} --", 190 d.name.display_string(), 191 d.val_help 192 ); 193 println!(); 194 for line in d.docs.lines().map(|s| s.trim()) { 195 let line = line.trim(); 196 println!(" {line}"); 197 } 198 } 199 std::process::exit(0); 200 } 201 202 let mut result = Vec::new(); 203 for val in val.split(',') { 204 // Split `k=v` into `k` and `v` where `v` is optional 205 let mut iter = val.splitn(2, '='); 206 let key = iter.next().unwrap(); 207 let key_val = iter.next(); 208 209 // Find `key` within `T::OPTIONS` 210 let option = options 211 .iter() 212 .filter_map(|d| match d.name { 213 OptName::Name(s) => { 214 let s = s.replace('_', "-"); 215 if s == key { 216 Some((d, s)) 217 } else { 218 None 219 } 220 } 221 OptName::Prefix(s) => { 222 let name = key.strip_prefix(s)?.strip_prefix("-")?; 223 Some((d, name.to_string())) 224 } 225 }) 226 .next(); 227 228 let (desc, key) = match option { 229 Some(pair) => pair, 230 None => { 231 let err = Error::raw( 232 ErrorKind::InvalidValue, 233 format!("unknown -{arg_short} / --{arg_long} option: {key}\n"), 234 ); 235 return Err(err.with_cmd(cmd)); 236 } 237 }; 238 239 result.push((desc.parse)(&key, key_val).map_err(|e| { 240 Error::raw( 241 ErrorKind::InvalidValue, 242 format!("failed to parse -{arg_short} option `{val}`: {e:?}\n"), 243 ) 244 .with_cmd(cmd) 245 })?) 246 } 247 248 Ok(CommaSeparated(result)) 249 } 250 } 251 252 /// Helper trait used by `CommaSeparated` which contains a list of all options 253 /// supported by the option group. 254 pub trait WasmtimeOption: Sized + Send + Sync + Clone + 'static { 255 const OPTIONS: &'static [OptionDesc<Self>]; 256 } 257 258 pub struct OptionDesc<T> { 259 pub name: OptName, 260 pub docs: &'static str, 261 pub parse: fn(&str, Option<&str>) -> Result<T>, 262 pub val_help: &'static str, 263 } 264 265 pub enum OptName { 266 /// A named option. Note that the `str` here uses `_` instead of `-` because 267 /// it's derived from Rust syntax. 268 Name(&'static str), 269 270 /// A prefixed option which strips the specified `name`, then `-`. 271 Prefix(&'static str), 272 } 273 274 impl OptName { 275 fn display_string(&self) -> String { 276 match self { 277 OptName::Name(s) => s.replace('_', "-"), 278 OptName::Prefix(s) => format!("{s}-<KEY>"), 279 } 280 } 281 } 282 283 /// A helper trait for all types of options that can be parsed. This is what 284 /// actually parses the `=val` in `key=val` 285 pub trait WasmtimeOptionValue: Sized { 286 /// Help text for the value to be specified. 287 const VAL_HELP: &'static str; 288 289 /// Parses the provided value, if given, returning an error on failure. 290 fn parse(val: Option<&str>) -> Result<Self>; 291 } 292 293 impl WasmtimeOptionValue for String { 294 const VAL_HELP: &'static str = "=val"; 295 fn parse(val: Option<&str>) -> Result<Self> { 296 match val { 297 Some(val) => Ok(val.to_string()), 298 None => bail!("value must be specified with `key=val` syntax"), 299 } 300 } 301 } 302 303 impl WasmtimeOptionValue for u32 { 304 const VAL_HELP: &'static str = "=N"; 305 fn parse(val: Option<&str>) -> Result<Self> { 306 let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, ""); 307 match val.strip_prefix("0x") { 308 Some(hex) => Ok(u32::from_str_radix(hex, 16)?), 309 None => Ok(val.parse()?), 310 } 311 } 312 } 313 314 impl WasmtimeOptionValue for u64 { 315 const VAL_HELP: &'static str = "=N"; 316 fn parse(val: Option<&str>) -> Result<Self> { 317 let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, ""); 318 match val.strip_prefix("0x") { 319 Some(hex) => Ok(u64::from_str_radix(hex, 16)?), 320 None => Ok(val.parse()?), 321 } 322 } 323 } 324 325 impl WasmtimeOptionValue for usize { 326 const VAL_HELP: &'static str = "=N"; 327 fn parse(val: Option<&str>) -> Result<Self> { 328 let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, ""); 329 match val.strip_prefix("0x") { 330 Some(hex) => Ok(usize::from_str_radix(hex, 16)?), 331 None => Ok(val.parse()?), 332 } 333 } 334 } 335 336 impl WasmtimeOptionValue for bool { 337 const VAL_HELP: &'static str = "[=y|n]"; 338 fn parse(val: Option<&str>) -> Result<Self> { 339 match val { 340 None | Some("y") | Some("yes") | Some("true") => Ok(true), 341 Some("n") | Some("no") | Some("false") => Ok(false), 342 Some(s) => bail!("unknown boolean flag `{s}`, only yes,no,<nothing> accepted"), 343 } 344 } 345 } 346 347 impl WasmtimeOptionValue for Duration { 348 const VAL_HELP: &'static str = "=N|Ns|Nms|.."; 349 fn parse(val: Option<&str>) -> Result<Duration> { 350 let s = String::parse(val)?; 351 // assume an integer without a unit specified is a number of seconds ... 352 if let Ok(val) = s.parse() { 353 return Ok(Duration::from_secs(val)); 354 } 355 // ... otherwise try to parse it with units such as `3s` or `300ms` 356 let dur = humantime::parse_duration(&s)?; 357 Ok(dur) 358 } 359 } 360 361 impl WasmtimeOptionValue for wasmtime::OptLevel { 362 const VAL_HELP: &'static str = "=0|1|2|s"; 363 fn parse(val: Option<&str>) -> Result<Self> { 364 match String::parse(val)?.as_str() { 365 "0" => Ok(wasmtime::OptLevel::None), 366 "1" => Ok(wasmtime::OptLevel::Speed), 367 "2" => Ok(wasmtime::OptLevel::Speed), 368 "s" => Ok(wasmtime::OptLevel::SpeedAndSize), 369 other => bail!( 370 "unknown optimization level `{}`, only 0,1,2,s accepted", 371 other 372 ), 373 } 374 } 375 } 376 377 impl WasmtimeOptionValue for wasmtime::RegallocAlgorithm { 378 const VAL_HELP: &'static str = "=backtracking|single-pass"; 379 fn parse(val: Option<&str>) -> Result<Self> { 380 match String::parse(val)?.as_str() { 381 "backtracking" => Ok(wasmtime::RegallocAlgorithm::Backtracking), 382 "single-pass" => Ok(wasmtime::RegallocAlgorithm::SinglePass), 383 other => bail!( 384 "unknown regalloc algorithm`{}`, only backtracking,single-pass accepted", 385 other 386 ), 387 } 388 } 389 } 390 391 impl WasmtimeOptionValue for wasmtime::Strategy { 392 const VAL_HELP: &'static str = "=winch|cranelift"; 393 fn parse(val: Option<&str>) -> Result<Self> { 394 match String::parse(val)?.as_str() { 395 "cranelift" => Ok(wasmtime::Strategy::Cranelift), 396 "winch" => Ok(wasmtime::Strategy::Winch), 397 other => bail!("unknown compiler `{other}` only `cranelift` and `winch` accepted",), 398 } 399 } 400 } 401 402 impl WasmtimeOptionValue for wasmtime::Collector { 403 const VAL_HELP: &'static str = "=drc|null"; 404 fn parse(val: Option<&str>) -> Result<Self> { 405 match String::parse(val)?.as_str() { 406 "drc" => Ok(wasmtime::Collector::DeferredReferenceCounting), 407 "null" => Ok(wasmtime::Collector::Null), 408 other => bail!("unknown collector `{other}` only `drc` and `null` accepted",), 409 } 410 } 411 } 412 413 impl WasmtimeOptionValue for WasiNnGraph { 414 const VAL_HELP: &'static str = "=<format>::<dir>"; 415 fn parse(val: Option<&str>) -> Result<Self> { 416 let val = String::parse(val)?; 417 let mut parts = val.splitn(2, "::"); 418 Ok(WasiNnGraph { 419 format: parts.next().unwrap().to_string(), 420 dir: match parts.next() { 421 Some(part) => part.into(), 422 None => bail!("graph does not contain `::` separator for directory"), 423 }, 424 }) 425 } 426 } 427 428 impl WasmtimeOptionValue for KeyValuePair { 429 const VAL_HELP: &'static str = "=<name>=<val>"; 430 fn parse(val: Option<&str>) -> Result<Self> { 431 let val = String::parse(val)?; 432 let mut parts = val.splitn(2, "="); 433 Ok(KeyValuePair { 434 key: parts.next().unwrap().to_string(), 435 value: match parts.next() { 436 Some(part) => part.into(), 437 None => "".to_string(), 438 }, 439 }) 440 } 441 } 442 443 #[cfg(test)] 444 mod tests { 445 use super::WasmtimeOptionValue; 446 447 #[test] 448 fn numbers_with_underscores() { 449 assert!(<u32 as WasmtimeOptionValue>::parse(Some("123")).is_ok_and(|v| v == 123)); 450 assert!(<u32 as WasmtimeOptionValue>::parse(Some("1_2_3")).is_ok_and(|v| v == 123)); 451 } 452 } 453