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