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