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