xref: /wasmtime-44.0.1/crates/cli-flags/src/opt.rs (revision eb2602dc)
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::marker;
13 use std::time::Duration;
14 
15 /// Characters which can be safely ignored while parsing numeric options to wasmtime
16 const IGNORED_NUMBER_CHARS: [char; 1] = ['_'];
17 
18 #[macro_export]
19 macro_rules! wasmtime_option_group {
20     (
21         $(#[$attr:meta])*
22         pub struct $opts:ident {
23             $(
24                 $(#[doc = $doc:tt])*
25                 pub $opt:ident: $container:ident<$payload:ty>,
26             )+
27 
28             $(
29                 #[prefixed = $prefix:tt]
30                 $(#[doc = $prefixed_doc:tt])*
31                 pub $prefixed:ident: Vec<(String, Option<String>)>,
32             )?
33         }
34         enum $option:ident {
35             ...
36         }
37     ) => {
38         #[derive(Default, Debug)]
39         $(#[$attr])*
40         pub struct $opts {
41             $(
42                 pub $opt: $container<$payload>,
43             )+
44             $(
45                 pub $prefixed: Vec<(String, Option<String>)>,
46             )?
47         }
48 
49         #[derive(Clone, Debug,PartialEq)]
50         #[expect(non_camel_case_types, reason = "macro-generated code")]
51         enum $option {
52             $(
53                 $opt($payload),
54             )+
55             $(
56                 $prefixed(String, Option<String>),
57             )?
58         }
59 
60         impl $crate::opt::WasmtimeOption for $option {
61             const OPTIONS: &'static [$crate::opt::OptionDesc<$option>] = &[
62                 $(
63                     $crate::opt::OptionDesc {
64                         name: $crate::opt::OptName::Name(stringify!($opt)),
65                         parse: |_, s| {
66                             Ok($option::$opt(
67                                 $crate::opt::WasmtimeOptionValue::parse(s)?
68                             ))
69                         },
70                         val_help: <$payload as $crate::opt::WasmtimeOptionValue>::VAL_HELP,
71                         docs: concat!($($doc, "\n",)*),
72                     },
73                  )+
74                 $(
75                     $crate::opt::OptionDesc {
76                         name: $crate::opt::OptName::Prefix($prefix),
77                         parse: |name, val| {
78                             Ok($option::$prefixed(
79                                 name.to_string(),
80                                 val.map(|v| v.to_string()),
81                             ))
82                         },
83                         val_help: "[=val]",
84                         docs: concat!($($prefixed_doc, "\n",)*),
85                     },
86                  )?
87             ];
88         }
89 
90         impl $opts {
91             fn configure_with(&mut self, opts: &[$crate::opt::CommaSeparated<$option>]) {
92                 for opt in opts.iter().flat_map(|o| o.0.iter()) {
93                     match opt {
94                         $(
95                             $option::$opt(val) => {
96                                 let dst = &mut self.$opt;
97                                 wasmtime_option_group!(@push $container dst val);
98                             }
99                         )+
100                         $(
101                             $option::$prefixed(key, val) => self.$prefixed.push((key.clone(), val.clone())),
102                         )?
103                     }
104                 }
105             }
106         }
107     };
108 
109     (@push Option $dst:ident $val:ident) => (*$dst = Some($val.clone()));
110     (@push Vec $dst:ident $val:ident) => ($dst.push($val.clone()));
111 }
112 
113 /// Parser registered with clap which handles parsing the `...` in `-O ...`.
114 #[derive(Clone, Debug, PartialEq)]
115 pub struct CommaSeparated<T>(pub Vec<T>);
116 
117 impl<T> ValueParserFactory for CommaSeparated<T>
118 where
119     T: WasmtimeOption,
120 {
121     type Parser = CommaSeparatedParser<T>;
122 
123     fn value_parser() -> CommaSeparatedParser<T> {
124         CommaSeparatedParser(marker::PhantomData)
125     }
126 }
127 
128 #[derive(Clone)]
129 pub struct CommaSeparatedParser<T>(marker::PhantomData<T>);
130 
131 impl<T> TypedValueParser for CommaSeparatedParser<T>
132 where
133     T: WasmtimeOption,
134 {
135     type Value = CommaSeparated<T>;
136 
137     fn parse_ref(
138         &self,
139         cmd: &clap::Command,
140         arg: Option<&clap::Arg>,
141         value: &std::ffi::OsStr,
142     ) -> Result<Self::Value, Error> {
143         let val = StringValueParser::new().parse_ref(cmd, arg, value)?;
144 
145         let options = T::OPTIONS;
146         let arg = arg.expect("should always have an argument");
147         let arg_long = arg.get_long().expect("should have a long name specified");
148         let arg_short = arg.get_short().expect("should have a short name specified");
149 
150         // Handle `-O help` which dumps all the `-O` options, their messages,
151         // and then exits.
152         if val == "help" {
153             let mut max = 0;
154             for d in options {
155                 max = max.max(d.name.display_string().len() + d.val_help.len());
156             }
157             println!("Available {arg_long} options:\n");
158             for d in options {
159                 print!(
160                     "  -{arg_short} {:>1$}",
161                     d.name.display_string(),
162                     max - d.val_help.len()
163                 );
164                 print!("{}", d.val_help);
165                 print!(" --");
166                 if val == "help" {
167                     for line in d.docs.lines().map(|s| s.trim()) {
168                         if line.is_empty() {
169                             break;
170                         }
171                         print!(" {line}");
172                     }
173                     println!();
174                 } else {
175                     println!();
176                     for line in d.docs.lines().map(|s| s.trim()) {
177                         let line = line.trim();
178                         println!("        {line}");
179                     }
180                 }
181             }
182             println!("\npass `-{arg_short} help-long` to see longer-form explanations");
183             std::process::exit(0);
184         }
185         if val == "help-long" {
186             println!("Available {arg_long} options:\n");
187             for d in options {
188                 println!(
189                     "  -{arg_short} {}{} --",
190                     d.name.display_string(),
191                     d.val_help
192                 );
193                 println!();
194                 for line in d.docs.lines().map(|s| s.trim()) {
195                     let line = line.trim();
196                     println!("        {line}");
197                 }
198             }
199             std::process::exit(0);
200         }
201 
202         let mut result = Vec::new();
203         for val in val.split(',') {
204             // Split `k=v` into `k` and `v` where `v` is optional
205             let mut iter = val.splitn(2, '=');
206             let key = iter.next().unwrap();
207             let key_val = iter.next();
208 
209             // Find `key` within `T::OPTIONS`
210             let option = options
211                 .iter()
212                 .filter_map(|d| match d.name {
213                     OptName::Name(s) => {
214                         let s = s.replace('_', "-");
215                         if s == key {
216                             Some((d, s))
217                         } else {
218                             None
219                         }
220                     }
221                     OptName::Prefix(s) => {
222                         let name = key.strip_prefix(s)?.strip_prefix("-")?;
223                         Some((d, name.to_string()))
224                     }
225                 })
226                 .next();
227 
228             let (desc, key) = match option {
229                 Some(pair) => pair,
230                 None => {
231                     let err = Error::raw(
232                         ErrorKind::InvalidValue,
233                         format!("unknown -{arg_short} / --{arg_long} option: {key}\n"),
234                     );
235                     return Err(err.with_cmd(cmd));
236                 }
237             };
238 
239             result.push((desc.parse)(&key, key_val).map_err(|e| {
240                 Error::raw(
241                     ErrorKind::InvalidValue,
242                     format!("failed to parse -{arg_short} option `{val}`: {e:?}\n"),
243                 )
244                 .with_cmd(cmd)
245             })?)
246         }
247 
248         Ok(CommaSeparated(result))
249     }
250 }
251 
252 /// Helper trait used by `CommaSeparated` which contains a list of all options
253 /// supported by the option group.
254 pub trait WasmtimeOption: Sized + Send + Sync + Clone + 'static {
255     const OPTIONS: &'static [OptionDesc<Self>];
256 }
257 
258 pub struct OptionDesc<T> {
259     pub name: OptName,
260     pub docs: &'static str,
261     pub parse: fn(&str, Option<&str>) -> Result<T>,
262     pub val_help: &'static str,
263 }
264 
265 pub enum OptName {
266     /// A named option. Note that the `str` here uses `_` instead of `-` because
267     /// it's derived from Rust syntax.
268     Name(&'static str),
269 
270     /// A prefixed option which strips the specified `name`, then `-`.
271     Prefix(&'static str),
272 }
273 
274 impl OptName {
275     fn display_string(&self) -> String {
276         match self {
277             OptName::Name(s) => s.replace('_', "-"),
278             OptName::Prefix(s) => format!("{s}-<KEY>"),
279         }
280     }
281 }
282 
283 /// A helper trait for all types of options that can be parsed. This is what
284 /// actually parses the `=val` in `key=val`
285 pub trait WasmtimeOptionValue: Sized {
286     /// Help text for the value to be specified.
287     const VAL_HELP: &'static str;
288 
289     /// Parses the provided value, if given, returning an error on failure.
290     fn parse(val: Option<&str>) -> Result<Self>;
291 }
292 
293 impl WasmtimeOptionValue for String {
294     const VAL_HELP: &'static str = "=val";
295     fn parse(val: Option<&str>) -> Result<Self> {
296         match val {
297             Some(val) => Ok(val.to_string()),
298             None => bail!("value must be specified with `key=val` syntax"),
299         }
300     }
301 }
302 
303 impl WasmtimeOptionValue for u32 {
304     const VAL_HELP: &'static str = "=N";
305     fn parse(val: Option<&str>) -> Result<Self> {
306         let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, "");
307         match val.strip_prefix("0x") {
308             Some(hex) => Ok(u32::from_str_radix(hex, 16)?),
309             None => Ok(val.parse()?),
310         }
311     }
312 }
313 
314 impl WasmtimeOptionValue for u64 {
315     const VAL_HELP: &'static str = "=N";
316     fn parse(val: Option<&str>) -> Result<Self> {
317         let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, "");
318         match val.strip_prefix("0x") {
319             Some(hex) => Ok(u64::from_str_radix(hex, 16)?),
320             None => Ok(val.parse()?),
321         }
322     }
323 }
324 
325 impl WasmtimeOptionValue for usize {
326     const VAL_HELP: &'static str = "=N";
327     fn parse(val: Option<&str>) -> Result<Self> {
328         let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, "");
329         match val.strip_prefix("0x") {
330             Some(hex) => Ok(usize::from_str_radix(hex, 16)?),
331             None => Ok(val.parse()?),
332         }
333     }
334 }
335 
336 impl WasmtimeOptionValue for bool {
337     const VAL_HELP: &'static str = "[=y|n]";
338     fn parse(val: Option<&str>) -> Result<Self> {
339         match val {
340             None | Some("y") | Some("yes") | Some("true") => Ok(true),
341             Some("n") | Some("no") | Some("false") => Ok(false),
342             Some(s) => bail!("unknown boolean flag `{s}`, only yes,no,<nothing> accepted"),
343         }
344     }
345 }
346 
347 impl WasmtimeOptionValue for Duration {
348     const VAL_HELP: &'static str = "=N|Ns|Nms|..";
349     fn parse(val: Option<&str>) -> Result<Duration> {
350         let s = String::parse(val)?;
351         // assume an integer without a unit specified is a number of seconds ...
352         if let Ok(val) = s.parse() {
353             return Ok(Duration::from_secs(val));
354         }
355         // ... otherwise try to parse it with units such as `3s` or `300ms`
356         let dur = humantime::parse_duration(&s)?;
357         Ok(dur)
358     }
359 }
360 
361 impl WasmtimeOptionValue for wasmtime::OptLevel {
362     const VAL_HELP: &'static str = "=0|1|2|s";
363     fn parse(val: Option<&str>) -> Result<Self> {
364         match String::parse(val)?.as_str() {
365             "0" => Ok(wasmtime::OptLevel::None),
366             "1" => Ok(wasmtime::OptLevel::Speed),
367             "2" => Ok(wasmtime::OptLevel::Speed),
368             "s" => Ok(wasmtime::OptLevel::SpeedAndSize),
369             other => bail!(
370                 "unknown optimization level `{}`, only 0,1,2,s accepted",
371                 other
372             ),
373         }
374     }
375 }
376 
377 impl WasmtimeOptionValue for wasmtime::RegallocAlgorithm {
378     const VAL_HELP: &'static str = "=backtracking|single-pass";
379     fn parse(val: Option<&str>) -> Result<Self> {
380         match String::parse(val)?.as_str() {
381             "backtracking" => Ok(wasmtime::RegallocAlgorithm::Backtracking),
382             "single-pass" => Ok(wasmtime::RegallocAlgorithm::SinglePass),
383             other => bail!(
384                 "unknown regalloc algorithm`{}`, only backtracking,single-pass accepted",
385                 other
386             ),
387         }
388     }
389 }
390 
391 impl WasmtimeOptionValue for wasmtime::Strategy {
392     const VAL_HELP: &'static str = "=winch|cranelift";
393     fn parse(val: Option<&str>) -> Result<Self> {
394         match String::parse(val)?.as_str() {
395             "cranelift" => Ok(wasmtime::Strategy::Cranelift),
396             "winch" => Ok(wasmtime::Strategy::Winch),
397             other => bail!("unknown compiler `{other}` only `cranelift` and `winch` accepted",),
398         }
399     }
400 }
401 
402 impl WasmtimeOptionValue for wasmtime::Collector {
403     const VAL_HELP: &'static str = "=drc|null";
404     fn parse(val: Option<&str>) -> Result<Self> {
405         match String::parse(val)?.as_str() {
406             "drc" => Ok(wasmtime::Collector::DeferredReferenceCounting),
407             "null" => Ok(wasmtime::Collector::Null),
408             other => bail!("unknown collector `{other}` only `drc` and `null` accepted",),
409         }
410     }
411 }
412 
413 impl WasmtimeOptionValue for WasiNnGraph {
414     const VAL_HELP: &'static str = "=<format>::<dir>";
415     fn parse(val: Option<&str>) -> Result<Self> {
416         let val = String::parse(val)?;
417         let mut parts = val.splitn(2, "::");
418         Ok(WasiNnGraph {
419             format: parts.next().unwrap().to_string(),
420             dir: match parts.next() {
421                 Some(part) => part.into(),
422                 None => bail!("graph does not contain `::` separator for directory"),
423             },
424         })
425     }
426 }
427 
428 impl WasmtimeOptionValue for KeyValuePair {
429     const VAL_HELP: &'static str = "=<name>=<val>";
430     fn parse(val: Option<&str>) -> Result<Self> {
431         let val = String::parse(val)?;
432         let mut parts = val.splitn(2, "=");
433         Ok(KeyValuePair {
434             key: parts.next().unwrap().to_string(),
435             value: match parts.next() {
436                 Some(part) => part.into(),
437                 None => "".to_string(),
438             },
439         })
440     }
441 }
442 
443 #[cfg(test)]
444 mod tests {
445     use super::WasmtimeOptionValue;
446 
447     #[test]
448     fn numbers_with_underscores() {
449         assert!(<u32 as WasmtimeOptionValue>::parse(Some("123")).is_ok_and(|v| v == 123));
450         assert!(<u32 as WasmtimeOptionValue>::parse(Some("1_2_3")).is_ok_and(|v| v == 123));
451     }
452 }
453