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::fmt; 13 use std::marker; 14 use std::time::Duration; 15 16 /// Characters which can be safely ignored while parsing numeric options to wasmtime 17 const IGNORED_NUMBER_CHARS: [char; 1] = ['_']; 18 19 #[macro_export] 20 macro_rules! wasmtime_option_group { 21 ( 22 $(#[$attr:meta])* 23 pub struct $opts:ident { 24 $( 25 $(#[doc = $doc:tt])* 26 pub $opt:ident: $container:ident<$payload:ty>, 27 )+ 28 29 $( 30 #[prefixed = $prefix:tt] 31 $(#[doc = $prefixed_doc:tt])* 32 pub $prefixed:ident: Vec<(String, Option<String>)>, 33 )? 34 } 35 enum $option:ident { 36 ... 37 } 38 ) => { 39 #[derive(Default, Debug)] 40 $(#[$attr])* 41 pub struct $opts { 42 $( 43 pub $opt: $container<$payload>, 44 )+ 45 $( 46 pub $prefixed: Vec<(String, Option<String>)>, 47 )? 48 } 49 50 #[derive(Clone, PartialEq)] 51 #[expect(non_camel_case_types, reason = "macro-generated code")] 52 enum $option { 53 $( 54 $opt($payload), 55 )+ 56 $( 57 $prefixed(String, Option<String>), 58 )? 59 } 60 61 impl $crate::opt::WasmtimeOption for $option { 62 const OPTIONS: &'static [$crate::opt::OptionDesc<$option>] = &[ 63 $( 64 $crate::opt::OptionDesc { 65 name: $crate::opt::OptName::Name(stringify!($opt)), 66 parse: |_, s| { 67 Ok($option::$opt( 68 $crate::opt::WasmtimeOptionValue::parse(s)? 69 )) 70 }, 71 val_help: <$payload as $crate::opt::WasmtimeOptionValue>::VAL_HELP, 72 docs: concat!($($doc, "\n",)*), 73 }, 74 )+ 75 $( 76 $crate::opt::OptionDesc { 77 name: $crate::opt::OptName::Prefix($prefix), 78 parse: |name, val| { 79 Ok($option::$prefixed( 80 name.to_string(), 81 val.map(|v| v.to_string()), 82 )) 83 }, 84 val_help: "[=val]", 85 docs: concat!($($prefixed_doc, "\n",)*), 86 }, 87 )? 88 ]; 89 } 90 91 impl core::fmt::Display for $option { 92 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 93 match self { 94 $( 95 $option::$opt(val) => { 96 write!(f, "{}=", stringify!($opt).replace('_', "-"))?; 97 $crate::opt::WasmtimeOptionValue::display(val, f) 98 } 99 )+ 100 $( 101 $option::$prefixed(key, val) => { 102 write!(f, "{}-{key}", stringify!($prefixed))?; 103 if let Some(val) = val { 104 write!(f, "={val}")?; 105 } 106 Ok(()) 107 } 108 )? 109 } 110 } 111 } 112 113 impl $opts { 114 fn configure_with(&mut self, opts: &[$crate::opt::CommaSeparated<$option>]) { 115 for opt in opts.iter().flat_map(|o| o.0.iter()) { 116 match opt { 117 $( 118 $option::$opt(val) => { 119 $crate::opt::OptionContainer::push(&mut self.$opt, val.clone()); 120 } 121 )+ 122 $( 123 $option::$prefixed(key, val) => self.$prefixed.push((key.clone(), val.clone())), 124 )? 125 } 126 } 127 } 128 129 fn to_options(&self) -> Vec<$option> { 130 let mut ret = Vec::new(); 131 $( 132 for item in $crate::opt::OptionContainer::get(&self.$opt) { 133 ret.push($option::$opt(item.clone())); 134 } 135 )+ 136 $( 137 for (key,val) in self.$prefixed.iter() { 138 ret.push($option::$prefixed(key.clone(), val.clone())); 139 } 140 )? 141 ret 142 } 143 } 144 }; 145 } 146 147 /// Parser registered with clap which handles parsing the `...` in `-O ...`. 148 #[derive(Clone, Debug, PartialEq)] 149 pub struct CommaSeparated<T>(pub Vec<T>); 150 151 impl<T> ValueParserFactory for CommaSeparated<T> 152 where 153 T: WasmtimeOption, 154 { 155 type Parser = CommaSeparatedParser<T>; 156 157 fn value_parser() -> CommaSeparatedParser<T> { 158 CommaSeparatedParser(marker::PhantomData) 159 } 160 } 161 162 #[derive(Clone)] 163 pub struct CommaSeparatedParser<T>(marker::PhantomData<T>); 164 165 impl<T> TypedValueParser for CommaSeparatedParser<T> 166 where 167 T: WasmtimeOption, 168 { 169 type Value = CommaSeparated<T>; 170 171 fn parse_ref( 172 &self, 173 cmd: &clap::Command, 174 arg: Option<&clap::Arg>, 175 value: &std::ffi::OsStr, 176 ) -> Result<Self::Value, Error> { 177 let val = StringValueParser::new().parse_ref(cmd, arg, value)?; 178 179 let options = T::OPTIONS; 180 let arg = arg.expect("should always have an argument"); 181 let arg_long = arg.get_long().expect("should have a long name specified"); 182 let arg_short = arg.get_short().expect("should have a short name specified"); 183 184 // Handle `-O help` which dumps all the `-O` options, their messages, 185 // and then exits. 186 if val == "help" { 187 let mut max = 0; 188 for d in options { 189 max = max.max(d.name.display_string().len() + d.val_help.len()); 190 } 191 println!("Available {arg_long} options:\n"); 192 for d in options { 193 print!( 194 " -{arg_short} {:>1$}", 195 d.name.display_string(), 196 max - d.val_help.len() 197 ); 198 print!("{}", d.val_help); 199 print!(" --"); 200 if val == "help" { 201 for line in d.docs.lines().map(|s| s.trim()) { 202 if line.is_empty() { 203 break; 204 } 205 print!(" {line}"); 206 } 207 println!(); 208 } else { 209 println!(); 210 for line in d.docs.lines().map(|s| s.trim()) { 211 let line = line.trim(); 212 println!(" {line}"); 213 } 214 } 215 } 216 println!("\npass `-{arg_short} help-long` to see longer-form explanations"); 217 std::process::exit(0); 218 } 219 if val == "help-long" { 220 println!("Available {arg_long} options:\n"); 221 for d in options { 222 println!( 223 " -{arg_short} {}{} --", 224 d.name.display_string(), 225 d.val_help 226 ); 227 println!(); 228 for line in d.docs.lines().map(|s| s.trim()) { 229 let line = line.trim(); 230 println!(" {line}"); 231 } 232 } 233 std::process::exit(0); 234 } 235 236 let mut result = Vec::new(); 237 for val in val.split(',') { 238 // Split `k=v` into `k` and `v` where `v` is optional 239 let mut iter = val.splitn(2, '='); 240 let key = iter.next().unwrap(); 241 let key_val = iter.next(); 242 243 // Find `key` within `T::OPTIONS` 244 let option = options 245 .iter() 246 .filter_map(|d| match d.name { 247 OptName::Name(s) => { 248 let s = s.replace('_', "-"); 249 if s == key { 250 Some((d, s)) 251 } else { 252 None 253 } 254 } 255 OptName::Prefix(s) => { 256 let name = key.strip_prefix(s)?.strip_prefix("-")?; 257 Some((d, name.to_string())) 258 } 259 }) 260 .next(); 261 262 let (desc, key) = match option { 263 Some(pair) => pair, 264 None => { 265 let err = Error::raw( 266 ErrorKind::InvalidValue, 267 format!("unknown -{arg_short} / --{arg_long} option: {key}\n"), 268 ); 269 return Err(err.with_cmd(cmd)); 270 } 271 }; 272 273 result.push((desc.parse)(&key, key_val).map_err(|e| { 274 Error::raw( 275 ErrorKind::InvalidValue, 276 format!("failed to parse -{arg_short} option `{val}`: {e:?}\n"), 277 ) 278 .with_cmd(cmd) 279 })?) 280 } 281 282 Ok(CommaSeparated(result)) 283 } 284 } 285 286 /// Helper trait used by `CommaSeparated` which contains a list of all options 287 /// supported by the option group. 288 pub trait WasmtimeOption: Sized + Send + Sync + Clone + 'static { 289 const OPTIONS: &'static [OptionDesc<Self>]; 290 } 291 292 pub struct OptionDesc<T> { 293 pub name: OptName, 294 pub docs: &'static str, 295 pub parse: fn(&str, Option<&str>) -> Result<T>, 296 pub val_help: &'static str, 297 } 298 299 pub enum OptName { 300 /// A named option. Note that the `str` here uses `_` instead of `-` because 301 /// it's derived from Rust syntax. 302 Name(&'static str), 303 304 /// A prefixed option which strips the specified `name`, then `-`. 305 Prefix(&'static str), 306 } 307 308 impl OptName { 309 fn display_string(&self) -> String { 310 match self { 311 OptName::Name(s) => s.replace('_', "-"), 312 OptName::Prefix(s) => format!("{s}-<KEY>"), 313 } 314 } 315 } 316 317 /// A helper trait for all types of options that can be parsed. This is what 318 /// actually parses the `=val` in `key=val` 319 pub trait WasmtimeOptionValue: Sized { 320 /// Help text for the value to be specified. 321 const VAL_HELP: &'static str; 322 323 /// Parses the provided value, if given, returning an error on failure. 324 fn parse(val: Option<&str>) -> Result<Self>; 325 326 /// Write the value to `f` that would parse to `self`. 327 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result; 328 } 329 330 impl WasmtimeOptionValue for String { 331 const VAL_HELP: &'static str = "=val"; 332 fn parse(val: Option<&str>) -> Result<Self> { 333 match val { 334 Some(val) => Ok(val.to_string()), 335 None => bail!("value must be specified with `key=val` syntax"), 336 } 337 } 338 339 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 340 f.write_str(self) 341 } 342 } 343 344 impl WasmtimeOptionValue for u32 { 345 const VAL_HELP: &'static str = "=N"; 346 fn parse(val: Option<&str>) -> Result<Self> { 347 let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, ""); 348 match val.strip_prefix("0x") { 349 Some(hex) => Ok(u32::from_str_radix(hex, 16)?), 350 None => Ok(val.parse()?), 351 } 352 } 353 354 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 355 write!(f, "{self}") 356 } 357 } 358 359 impl WasmtimeOptionValue for u64 { 360 const VAL_HELP: &'static str = "=N"; 361 fn parse(val: Option<&str>) -> Result<Self> { 362 let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, ""); 363 match val.strip_prefix("0x") { 364 Some(hex) => Ok(u64::from_str_radix(hex, 16)?), 365 None => Ok(val.parse()?), 366 } 367 } 368 369 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 370 write!(f, "{self}") 371 } 372 } 373 374 impl WasmtimeOptionValue for usize { 375 const VAL_HELP: &'static str = "=N"; 376 fn parse(val: Option<&str>) -> Result<Self> { 377 let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, ""); 378 match val.strip_prefix("0x") { 379 Some(hex) => Ok(usize::from_str_radix(hex, 16)?), 380 None => Ok(val.parse()?), 381 } 382 } 383 384 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 385 write!(f, "{self}") 386 } 387 } 388 389 impl WasmtimeOptionValue for bool { 390 const VAL_HELP: &'static str = "[=y|n]"; 391 fn parse(val: Option<&str>) -> Result<Self> { 392 match val { 393 None | Some("y") | Some("yes") | Some("true") => Ok(true), 394 Some("n") | Some("no") | Some("false") => Ok(false), 395 Some(s) => bail!("unknown boolean flag `{s}`, only yes,no,<nothing> accepted"), 396 } 397 } 398 399 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 400 if *self { 401 f.write_str("y") 402 } else { 403 f.write_str("n") 404 } 405 } 406 } 407 408 impl WasmtimeOptionValue for Duration { 409 const VAL_HELP: &'static str = "=N|Ns|Nms|.."; 410 fn parse(val: Option<&str>) -> Result<Duration> { 411 let s = String::parse(val)?; 412 // assume an integer without a unit specified is a number of seconds ... 413 if let Ok(val) = s.parse() { 414 return Ok(Duration::from_secs(val)); 415 } 416 // ... otherwise try to parse it with units such as `3s` or `300ms` 417 let dur = humantime::parse_duration(&s)?; 418 Ok(dur) 419 } 420 421 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 422 write!(f, "{}", humantime::format_duration(*self)) 423 } 424 } 425 426 impl WasmtimeOptionValue for wasmtime::OptLevel { 427 const VAL_HELP: &'static str = "=0|1|2|s"; 428 fn parse(val: Option<&str>) -> Result<Self> { 429 match String::parse(val)?.as_str() { 430 "0" => Ok(wasmtime::OptLevel::None), 431 "1" => Ok(wasmtime::OptLevel::Speed), 432 "2" => Ok(wasmtime::OptLevel::Speed), 433 "s" => Ok(wasmtime::OptLevel::SpeedAndSize), 434 other => bail!( 435 "unknown optimization level `{}`, only 0,1,2,s accepted", 436 other 437 ), 438 } 439 } 440 441 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 442 match *self { 443 wasmtime::OptLevel::None => f.write_str("0"), 444 wasmtime::OptLevel::Speed => f.write_str("2"), 445 wasmtime::OptLevel::SpeedAndSize => f.write_str("s"), 446 _ => unreachable!(), 447 } 448 } 449 } 450 451 impl WasmtimeOptionValue for wasmtime::RegallocAlgorithm { 452 const VAL_HELP: &'static str = "=backtracking|single-pass"; 453 fn parse(val: Option<&str>) -> Result<Self> { 454 match String::parse(val)?.as_str() { 455 "backtracking" => Ok(wasmtime::RegallocAlgorithm::Backtracking), 456 "single-pass" => Ok(wasmtime::RegallocAlgorithm::SinglePass), 457 other => bail!( 458 "unknown regalloc algorithm`{}`, only backtracking,single-pass accepted", 459 other 460 ), 461 } 462 } 463 464 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 465 match *self { 466 wasmtime::RegallocAlgorithm::Backtracking => f.write_str("backtracking"), 467 wasmtime::RegallocAlgorithm::SinglePass => f.write_str("single-pass"), 468 _ => unreachable!(), 469 } 470 } 471 } 472 473 impl WasmtimeOptionValue for wasmtime::Strategy { 474 const VAL_HELP: &'static str = "=winch|cranelift"; 475 fn parse(val: Option<&str>) -> Result<Self> { 476 match String::parse(val)?.as_str() { 477 "cranelift" => Ok(wasmtime::Strategy::Cranelift), 478 "winch" => Ok(wasmtime::Strategy::Winch), 479 other => bail!("unknown compiler `{other}` only `cranelift` and `winch` accepted",), 480 } 481 } 482 483 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 484 match *self { 485 wasmtime::Strategy::Cranelift => f.write_str("cranelift"), 486 wasmtime::Strategy::Winch => f.write_str("winch"), 487 _ => unreachable!(), 488 } 489 } 490 } 491 492 impl WasmtimeOptionValue for wasmtime::Collector { 493 const VAL_HELP: &'static str = "=drc|null"; 494 fn parse(val: Option<&str>) -> Result<Self> { 495 match String::parse(val)?.as_str() { 496 "drc" => Ok(wasmtime::Collector::DeferredReferenceCounting), 497 "null" => Ok(wasmtime::Collector::Null), 498 other => bail!("unknown collector `{other}` only `drc` and `null` accepted",), 499 } 500 } 501 502 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 503 match *self { 504 wasmtime::Collector::DeferredReferenceCounting => f.write_str("drc"), 505 wasmtime::Collector::Null => f.write_str("null"), 506 _ => unreachable!(), 507 } 508 } 509 } 510 511 impl WasmtimeOptionValue for wasmtime::MpkEnabled { 512 const VAL_HELP: &'static str = "[=y|n|auto]"; 513 fn parse(val: Option<&str>) -> Result<Self> { 514 match val { 515 None | Some("y") | Some("yes") | Some("true") => Ok(wasmtime::MpkEnabled::Enable), 516 Some("n") | Some("no") | Some("false") => Ok(wasmtime::MpkEnabled::Disable), 517 Some("auto") => Ok(wasmtime::MpkEnabled::Auto), 518 Some(s) => bail!("unknown mpk flag `{s}`, only yes,no,auto,<nothing> accepted"), 519 } 520 } 521 522 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 523 match *self { 524 wasmtime::MpkEnabled::Enable => f.write_str("y"), 525 wasmtime::MpkEnabled::Disable => f.write_str("n"), 526 wasmtime::MpkEnabled::Auto => f.write_str("auto"), 527 } 528 } 529 } 530 531 impl WasmtimeOptionValue for WasiNnGraph { 532 const VAL_HELP: &'static str = "=<format>::<dir>"; 533 fn parse(val: Option<&str>) -> Result<Self> { 534 let val = String::parse(val)?; 535 let mut parts = val.splitn(2, "::"); 536 Ok(WasiNnGraph { 537 format: parts.next().unwrap().to_string(), 538 dir: match parts.next() { 539 Some(part) => part.into(), 540 None => bail!("graph does not contain `::` separator for directory"), 541 }, 542 }) 543 } 544 545 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 546 write!(f, "{}::{}", self.format, self.dir) 547 } 548 } 549 550 impl WasmtimeOptionValue for KeyValuePair { 551 const VAL_HELP: &'static str = "=<name>=<val>"; 552 fn parse(val: Option<&str>) -> Result<Self> { 553 let val = String::parse(val)?; 554 let mut parts = val.splitn(2, "="); 555 Ok(KeyValuePair { 556 key: parts.next().unwrap().to_string(), 557 value: match parts.next() { 558 Some(part) => part.into(), 559 None => "".to_string(), 560 }, 561 }) 562 } 563 564 fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 565 f.write_str(&self.key)?; 566 if !self.value.is_empty() { 567 f.write_str("=")?; 568 f.write_str(&self.value)?; 569 } 570 Ok(()) 571 } 572 } 573 574 pub trait OptionContainer<T> { 575 fn push(&mut self, val: T); 576 fn get<'a>(&'a self) -> impl Iterator<Item = &'a T> 577 where 578 T: 'a; 579 } 580 581 impl<T> OptionContainer<T> for Option<T> { 582 fn push(&mut self, val: T) { 583 *self = Some(val); 584 } 585 fn get<'a>(&'a self) -> impl Iterator<Item = &'a T> 586 where 587 T: 'a, 588 { 589 self.iter() 590 } 591 } 592 593 impl<T> OptionContainer<T> for Vec<T> { 594 fn push(&mut self, val: T) { 595 Vec::push(self, val); 596 } 597 fn get<'a>(&'a self) -> impl Iterator<Item = &'a T> 598 where 599 T: 'a, 600 { 601 self.iter() 602 } 603 } 604 605 #[cfg(test)] 606 mod tests { 607 use super::WasmtimeOptionValue; 608 609 #[test] 610 fn numbers_with_underscores() { 611 assert!(<u32 as WasmtimeOptionValue>::parse(Some("123")).is_ok_and(|v| v == 123)); 612 assert!(<u32 as WasmtimeOptionValue>::parse(Some("1_2_3")).is_ok_and(|v| v == 123)); 613 } 614 } 615