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