//! Support for parsing Wasmtime's `-O`, `-W`, etc "option groups" //! //! This builds up a clap-derive-like system where there's ideally a single //! macro `wasmtime_option_group!` which is invoked per-option which enables //! specifying options in a struct-like syntax where all other boilerplate about //! option parsing is contained exclusively within this module. use crate::{KeyValuePair, WasiNnGraph}; use clap::builder::{StringValueParser, TypedValueParser, ValueParserFactory}; use clap::error::{Error, ErrorKind}; use serde::de::{self, Visitor}; use std::path::PathBuf; use std::str::FromStr; use std::time::Duration; use std::{fmt, marker}; use wasmtime::{Result, bail}; /// Characters which can be safely ignored while parsing numeric options to wasmtime const IGNORED_NUMBER_CHARS: [char; 1] = ['_']; #[macro_export] macro_rules! wasmtime_option_group { ( $(#[$attr:meta])* pub struct $opts:ident { $( $(#[doc = $doc:tt])* $(#[doc($doc_attr:meta)])? $(#[serde($serde_attr:meta)])* pub $opt:ident: $container:ident<$payload:ty>, )+ $( #[prefixed = $prefix:tt] $(#[serde($serde_attr2:meta)])* $(#[doc = $prefixed_doc:tt])* $(#[doc($prefixed_doc_attr:meta)])? pub $prefixed:ident: Vec<(String, Option)>, )? } enum $option:ident { ... } ) => { #[derive(Default, Debug)] $(#[$attr])* pub struct $opts { $( $(#[serde($serde_attr)])* $(#[doc($doc_attr)])? pub $opt: $container<$payload>, )+ $( $(#[serde($serde_attr2)])* pub $prefixed: Vec<(String, Option)>, )? } #[derive(Clone, PartialEq)] #[expect(non_camel_case_types, reason = "macro-generated code")] enum $option { $( $opt($payload), )+ $( $prefixed(String, Option), )? } impl $crate::opt::WasmtimeOption for $option { const OPTIONS: &'static [$crate::opt::OptionDesc<$option>] = &[ $( $crate::opt::OptionDesc { name: $crate::opt::OptName::Name(stringify!($opt)), parse: |_, s| { Ok($option::$opt( $crate::opt::WasmtimeOptionValue::parse(s)? )) }, val_help: <$payload as $crate::opt::WasmtimeOptionValue>::VAL_HELP, docs: concat!($($doc, "\n",)*), }, )+ $( $crate::opt::OptionDesc { name: $crate::opt::OptName::Prefix($prefix), parse: |name, val| { Ok($option::$prefixed( name.to_string(), val.map(|v| v.to_string()), )) }, val_help: "[=val]", docs: concat!($($prefixed_doc, "\n",)*), }, )? ]; } impl core::fmt::Display for $option { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { $( $option::$opt(val) => { write!(f, "{}=", stringify!($opt).replace('_', "-"))?; $crate::opt::WasmtimeOptionValue::display(val, f) } )+ $( $option::$prefixed(key, val) => { write!(f, "{}-{key}", stringify!($prefixed))?; if let Some(val) = val { write!(f, "={val}")?; } Ok(()) } )? } } } impl $opts { fn configure_with(&mut self, opts: &[$crate::opt::CommaSeparated<$option>]) { for opt in opts.iter().flat_map(|o| o.0.iter()) { match opt { $( $option::$opt(val) => { $crate::opt::OptionContainer::push(&mut self.$opt, val.clone()); } )+ $( $option::$prefixed(key, val) => self.$prefixed.push((key.clone(), val.clone())), )? } } } fn to_options(&self) -> Vec<$option> { let mut ret = Vec::new(); $( for item in $crate::opt::OptionContainer::get(&self.$opt) { ret.push($option::$opt(item.clone())); } )+ $( for (key,val) in self.$prefixed.iter() { ret.push($option::$prefixed(key.clone(), val.clone())); } )? ret } } }; } /// Parser registered with clap which handles parsing the `...` in `-O ...`. #[derive(Clone, Debug, PartialEq)] pub struct CommaSeparated(pub Vec); impl ValueParserFactory for CommaSeparated where T: WasmtimeOption, { type Parser = CommaSeparatedParser; fn value_parser() -> CommaSeparatedParser { CommaSeparatedParser(marker::PhantomData) } } #[derive(Clone)] pub struct CommaSeparatedParser(marker::PhantomData); impl TypedValueParser for CommaSeparatedParser where T: WasmtimeOption, { type Value = CommaSeparated; fn parse_ref( &self, cmd: &clap::Command, arg: Option<&clap::Arg>, value: &std::ffi::OsStr, ) -> Result { let val = StringValueParser::new().parse_ref(cmd, arg, value)?; let options = T::OPTIONS; let arg = arg.expect("should always have an argument"); let arg_long = arg.get_long().expect("should have a long name specified"); let arg_short = arg.get_short().expect("should have a short name specified"); // Handle `-O help` which dumps all the `-O` options, their messages, // and then exits. if val == "help" { let mut max = 0; for d in options { max = max.max(d.name.display_string().len() + d.val_help.len()); } println!("Available {arg_long} options:\n"); for d in options { print!( " -{arg_short} {:>1$}", d.name.display_string(), max - d.val_help.len() ); print!("{}", d.val_help); print!(" --"); if val == "help" { for line in d.docs.lines().map(|s| s.trim()) { if line.is_empty() { break; } print!(" {line}"); } println!(); } else { println!(); for line in d.docs.lines().map(|s| s.trim()) { let line = line.trim(); println!(" {line}"); } } } println!("\npass `-{arg_short} help-long` to see longer-form explanations"); std::process::exit(0); } if val == "help-long" { println!("Available {arg_long} options:\n"); for d in options { println!( " -{arg_short} {}{} --", d.name.display_string(), d.val_help ); println!(); for line in d.docs.lines().map(|s| s.trim()) { let line = line.trim(); println!(" {line}"); } } std::process::exit(0); } let mut result = Vec::new(); for val in val.split(',') { // Split `k=v` into `k` and `v` where `v` is optional let mut iter = val.splitn(2, '='); let key = iter.next().unwrap(); let key_val = iter.next(); // Find `key` within `T::OPTIONS` let option = options .iter() .filter_map(|d| match d.name { OptName::Name(s) => { let s = s.replace('_', "-"); if s == key { Some((d, s)) } else { None } } OptName::Prefix(s) => { let name = key.strip_prefix(s)?.strip_prefix("-")?; Some((d, name.to_string())) } }) .next(); let (desc, key) = match option { Some(pair) => pair, None => { let err = Error::raw( ErrorKind::InvalidValue, format!("unknown -{arg_short} / --{arg_long} option: {key}\n"), ); return Err(err.with_cmd(cmd)); } }; result.push((desc.parse)(&key, key_val).map_err(|e| { Error::raw( ErrorKind::InvalidValue, format!("failed to parse -{arg_short} option `{val}`: {e:?}\n"), ) .with_cmd(cmd) })?) } Ok(CommaSeparated(result)) } } /// Helper trait used by `CommaSeparated` which contains a list of all options /// supported by the option group. pub trait WasmtimeOption: Sized + Send + Sync + Clone + 'static { const OPTIONS: &'static [OptionDesc]; } pub struct OptionDesc { pub name: OptName, pub docs: &'static str, pub parse: fn(&str, Option<&str>) -> Result, pub val_help: &'static str, } pub enum OptName { /// A named option. Note that the `str` here uses `_` instead of `-` because /// it's derived from Rust syntax. Name(&'static str), /// A prefixed option which strips the specified `name`, then `-`. Prefix(&'static str), } impl OptName { fn display_string(&self) -> String { match self { OptName::Name(s) => s.replace('_', "-"), OptName::Prefix(s) => format!("{s}-"), } } } /// A helper trait for all types of options that can be parsed. This is what /// actually parses the `=val` in `key=val` pub trait WasmtimeOptionValue: Sized { /// Help text for the value to be specified. const VAL_HELP: &'static str; /// Parses the provided value, if given, returning an error on failure. fn parse(val: Option<&str>) -> Result; /// Write the value to `f` that would parse to `self`. fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result; } impl WasmtimeOptionValue for String { const VAL_HELP: &'static str = "=val"; fn parse(val: Option<&str>) -> Result { match val { Some(val) => Ok(val.to_string()), None => bail!("value must be specified with `key=val` syntax"), } } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self) } } impl WasmtimeOptionValue for PathBuf { const VAL_HELP: &'static str = "=path"; fn parse(val: Option<&str>) -> Result { match val { Some(val) => Ok(PathBuf::from_str(val)?), None => bail!("value must be specified with key=val syntax"), } } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self:?}") } } impl WasmtimeOptionValue for u32 { const VAL_HELP: &'static str = "=N"; fn parse(val: Option<&str>) -> Result { let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, ""); match val.strip_prefix("0x") { Some(hex) => Ok(u32::from_str_radix(hex, 16)?), None => Ok(val.parse()?), } } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self}") } } impl WasmtimeOptionValue for u64 { const VAL_HELP: &'static str = "=N"; fn parse(val: Option<&str>) -> Result { let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, ""); match val.strip_prefix("0x") { Some(hex) => Ok(u64::from_str_radix(hex, 16)?), None => Ok(val.parse()?), } } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self}") } } impl WasmtimeOptionValue for usize { const VAL_HELP: &'static str = "=N"; fn parse(val: Option<&str>) -> Result { let val = String::parse(val)?.replace(IGNORED_NUMBER_CHARS, ""); match val.strip_prefix("0x") { Some(hex) => Ok(usize::from_str_radix(hex, 16)?), None => Ok(val.parse()?), } } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{self}") } } impl WasmtimeOptionValue for bool { const VAL_HELP: &'static str = "[=y|n]"; fn parse(val: Option<&str>) -> Result { match val { None | Some("y") | Some("yes") | Some("true") => Ok(true), Some("n") | Some("no") | Some("false") => Ok(false), Some(s) => bail!("unknown boolean flag `{s}`, only yes,no, accepted"), } } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if *self { f.write_str("y") } else { f.write_str("n") } } } impl WasmtimeOptionValue for Duration { const VAL_HELP: &'static str = "=N|Ns|Nms|.."; fn parse(val: Option<&str>) -> Result { let s = String::parse(val)?; // assume an integer without a unit specified is a number of seconds ... if let Ok(val) = s.parse() { return Ok(Duration::from_secs(val)); } if let Some(num) = s.strip_suffix("s") { if let Ok(val) = num.parse() { return Ok(Duration::from_secs(val)); } } if let Some(num) = s.strip_suffix("ms") { if let Ok(val) = num.parse() { return Ok(Duration::from_millis(val)); } } if let Some(num) = s.strip_suffix("us").or(s.strip_suffix("μs")) { if let Ok(val) = num.parse() { return Ok(Duration::from_micros(val)); } } if let Some(num) = s.strip_suffix("ns") { if let Ok(val) = num.parse() { return Ok(Duration::from_nanos(val)); } } bail!("failed to parse duration: {s}") } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let subsec = self.subsec_nanos(); if subsec == 0 { write!(f, "{}s", self.as_secs()) } else if subsec % 1_000 == 0 { write!(f, "{}μs", self.as_micros()) } else if subsec % 1_000_000 == 0 { write!(f, "{}ms", self.as_millis()) } else { write!(f, "{}ns", self.as_nanos()) } } } impl WasmtimeOptionValue for wasmtime::OptLevel { const VAL_HELP: &'static str = "=0|1|2|s"; fn parse(val: Option<&str>) -> Result { match String::parse(val)?.as_str() { "0" => Ok(wasmtime::OptLevel::None), "1" => Ok(wasmtime::OptLevel::Speed), "2" => Ok(wasmtime::OptLevel::Speed), "s" => Ok(wasmtime::OptLevel::SpeedAndSize), other => bail!("unknown optimization level `{other}`, only 0,1,2,s accepted"), } } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { wasmtime::OptLevel::None => f.write_str("0"), wasmtime::OptLevel::Speed => f.write_str("2"), wasmtime::OptLevel::SpeedAndSize => f.write_str("s"), _ => unreachable!(), } } } impl WasmtimeOptionValue for wasmtime::RegallocAlgorithm { const VAL_HELP: &'static str = "=backtracking|single-pass"; fn parse(val: Option<&str>) -> Result { match String::parse(val)?.as_str() { "backtracking" => Ok(wasmtime::RegallocAlgorithm::Backtracking), "single-pass" => Ok(wasmtime::RegallocAlgorithm::SinglePass), other => { bail!("unknown regalloc algorithm`{other}`, only backtracking,single-pass accepted") } } } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { wasmtime::RegallocAlgorithm::Backtracking => f.write_str("backtracking"), wasmtime::RegallocAlgorithm::SinglePass => f.write_str("single-pass"), _ => unreachable!(), } } } impl WasmtimeOptionValue for wasmtime::Strategy { const VAL_HELP: &'static str = "=winch|cranelift"; fn parse(val: Option<&str>) -> Result { match String::parse(val)?.as_str() { "cranelift" => Ok(wasmtime::Strategy::Cranelift), "winch" => Ok(wasmtime::Strategy::Winch), other => bail!("unknown compiler `{other}` only `cranelift` and `winch` accepted",), } } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { wasmtime::Strategy::Cranelift => f.write_str("cranelift"), wasmtime::Strategy::Winch => f.write_str("winch"), _ => unreachable!(), } } } impl WasmtimeOptionValue for wasmtime::Collector { const VAL_HELP: &'static str = "=drc|null"; fn parse(val: Option<&str>) -> Result { match String::parse(val)?.as_str() { "drc" => Ok(wasmtime::Collector::DeferredReferenceCounting), "null" => Ok(wasmtime::Collector::Null), other => bail!("unknown collector `{other}` only `drc` and `null` accepted",), } } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { wasmtime::Collector::DeferredReferenceCounting => f.write_str("drc"), wasmtime::Collector::Null => f.write_str("null"), _ => unreachable!(), } } } impl WasmtimeOptionValue for wasmtime::Enabled { const VAL_HELP: &'static str = "[=y|n|auto]"; fn parse(val: Option<&str>) -> Result { match val { None | Some("y") | Some("yes") | Some("true") => Ok(wasmtime::Enabled::Yes), Some("n") | Some("no") | Some("false") => Ok(wasmtime::Enabled::No), Some("auto") => Ok(wasmtime::Enabled::Auto), Some(s) => bail!("unknown flag `{s}`, only yes,no,auto, accepted"), } } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { wasmtime::Enabled::Yes => f.write_str("y"), wasmtime::Enabled::No => f.write_str("n"), wasmtime::Enabled::Auto => f.write_str("auto"), } } } impl WasmtimeOptionValue for WasiNnGraph { const VAL_HELP: &'static str = "=::"; fn parse(val: Option<&str>) -> Result { let val = String::parse(val)?; let mut parts = val.splitn(2, "::"); Ok(WasiNnGraph { format: parts.next().unwrap().to_string(), dir: match parts.next() { Some(part) => part.into(), None => bail!("graph does not contain `::` separator for directory"), }, }) } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}::{}", self.format, self.dir) } } impl WasmtimeOptionValue for KeyValuePair { const VAL_HELP: &'static str = "=="; fn parse(val: Option<&str>) -> Result { let val = String::parse(val)?; let mut parts = val.splitn(2, "="); Ok(KeyValuePair { key: parts.next().unwrap().to_string(), value: match parts.next() { Some(part) => part.into(), None => "".to_string(), }, }) } fn display(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.key)?; if !self.value.is_empty() { f.write_str("=")?; f.write_str(&self.value)?; } Ok(()) } } pub trait OptionContainer { fn push(&mut self, val: T); fn get<'a>(&'a self) -> impl Iterator where T: 'a; } impl OptionContainer for Option { fn push(&mut self, val: T) { *self = Some(val); } fn get<'a>(&'a self) -> impl Iterator where T: 'a, { self.iter() } } impl OptionContainer for Vec { fn push(&mut self, val: T) { Vec::push(self, val); } fn get<'a>(&'a self) -> impl Iterator where T: 'a, { self.iter() } } // Used to parse toml values into string so that we can reuse the `WasmtimeOptionValue::parse` // for parsing toml values the same way we parse command line values. // // Used for wasmtime::Strategy, wasmtime::Collector, wasmtime::OptLevel, wasmtime::RegallocAlgorithm struct ToStringVisitor {} impl<'de> Visitor<'de> for ToStringVisitor { type Value = String; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "&str, u64, or i64") } fn visit_str(self, s: &str) -> Result where E: de::Error, { Ok(s.to_owned()) } fn visit_u64(self, v: u64) -> Result where E: de::Error, { Ok(v.to_string()) } fn visit_i64(self, v: i64) -> Result where E: de::Error, { Ok(v.to_string()) } } // Deserializer that uses the `WasmtimeOptionValue::parse` to parse toml values pub(crate) fn cli_parse_wrapper<'de, D, T>(deserializer: D) -> Result, D::Error> where T: WasmtimeOptionValue, D: serde::Deserializer<'de>, { let to_string_visitor = ToStringVisitor {}; let str = deserializer.deserialize_any(to_string_visitor)?; T::parse(Some(&str)) .map(Some) .map_err(serde::de::Error::custom) } #[cfg(test)] mod tests { use super::WasmtimeOptionValue; #[test] fn numbers_with_underscores() { assert!(::parse(Some("123")).is_ok_and(|v| v == 123)); assert!(::parse(Some("1_2_3")).is_ok_and(|v| v == 123)); } }