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