Skip to main content

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