1use 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
22const 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#[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 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 let mut iter = val.splitn(2, '=');
258 let key = iter.next().unwrap();
259 let key_val = iter.next();
260
261 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
300pub 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 Name(&'static str),
317
318 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
332pub trait WasmtimeOptionValue: Sized {
335 const VAL_HELP: &'static str;
337
338 fn parse(val: Option<&str>) -> Result<Self>;
340
341 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 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#[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#[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}