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