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