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