Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/crates/cli-flags/src/opt.rs
1692 views
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
8
use crate::{KeyValuePair, WasiNnGraph};
9
use anyhow::{Result, bail};
10
use clap::builder::{StringValueParser, TypedValueParser, ValueParserFactory};
11
use clap::error::{Error, ErrorKind};
12
use serde::de::{self, Visitor};
13
use std::time::Duration;
14
use std::{fmt, marker};
15
16
/// Characters which can be safely ignored while parsing numeric options to wasmtime
17
const IGNORED_NUMBER_CHARS: [char; 1] = ['_'];
18
19
#[macro_export]
20
macro_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
/// Parser registered with clap which handles parsing the `...` in `-O ...`.
155
#[derive(Clone, Debug, PartialEq)]
156
pub struct CommaSeparated<T>(pub Vec<T>);
157
158
impl<T> ValueParserFactory for CommaSeparated<T>
159
where
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)]
170
pub struct CommaSeparatedParser<T>(marker::PhantomData<T>);
171
172
impl<T> TypedValueParser for CommaSeparatedParser<T>
173
where
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
// Handle `-O help` which dumps all the `-O` options, their messages,
192
// and then exits.
193
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
// Split `k=v` into `k` and `v` where `v` is optional
246
let mut iter = val.splitn(2, '=');
247
let key = iter.next().unwrap();
248
let key_val = iter.next();
249
250
// Find `key` within `T::OPTIONS`
251
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
289
/// Helper trait used by `CommaSeparated` which contains a list of all options
290
/// supported by the option group.
291
pub trait WasmtimeOption: Sized + Send + Sync + Clone + 'static {
292
const OPTIONS: &'static [OptionDesc<Self>];
293
}
294
295
pub 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
302
pub enum OptName {
303
/// A named option. Note that the `str` here uses `_` instead of `-` because
304
/// it's derived from Rust syntax.
305
Name(&'static str),
306
307
/// A prefixed option which strips the specified `name`, then `-`.
308
Prefix(&'static str),
309
}
310
311
impl 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
320
/// A helper trait for all types of options that can be parsed. This is what
321
/// actually parses the `=val` in `key=val`
322
pub trait WasmtimeOptionValue: Sized {
323
/// Help text for the value to be specified.
324
const VAL_HELP: &'static str;
325
326
/// Parses the provided value, if given, returning an error on failure.
327
fn parse(val: Option<&str>) -> Result<Self>;
328
329
/// Write the value to `f` that would parse to `self`.
330
fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
331
}
332
333
impl 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
347
impl 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
362
impl 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
377
impl 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
392
impl 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
411
impl 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
// assume an integer without a unit specified is a number of seconds ...
416
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
458
impl 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
483
impl 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
505
impl 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
524
impl 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
543
impl 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
563
impl 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
582
impl 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
606
pub 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
613
impl<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
625
impl<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
637
// Used to parse toml values into string so that we can reuse the `WasmtimeOptionValue::parse`
638
// for parsing toml values the same way we parse command line values.
639
//
640
// Used for wasmtime::Strategy, wasmtime::Collector, wasmtime::OptLevel, wasmtime::RegallocAlgorithm
641
struct ToStringVisitor {}
642
643
impl<'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
672
// Deserializer that uses the `WasmtimeOptionValue::parse` to parse toml values
673
pub(crate) fn cli_parse_wrapper<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
674
where
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)]
687
mod 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
}
696
697