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