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