xref: /wasmtime-44.0.1/crates/cli-flags/src/opt.rs (revision bfcf4e3f)
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