wasmtime_cli_flags/
opt.rs

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