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