1use std::collections::BTreeMap;
19
20use anyhow::{Result, anyhow, bail};
21
22#[derive(Clone, Debug, PartialEq)]
24pub enum ParamKind {
25 Bool,
26 Int {
27 min: i64,
28 max: i64,
29 },
30 Float {
31 min: f64,
32 max: f64,
33 },
34 Text,
36 Choice(&'static [&'static str]),
39 IntList {
41 min: i64,
42 max: i64,
43 },
44}
45
46#[derive(Clone, Debug, PartialEq)]
48pub enum ParamValue {
49 Bool(bool),
50 Int(i64),
51 Float(f64),
52 Text(String),
53 IntList(Vec<i64>),
54}
55
56impl ParamValue {
57 pub fn to_edit_string(&self) -> String {
60 match self {
61 ParamValue::Bool(b) => b.to_string(),
62 ParamValue::Int(i) => i.to_string(),
63 ParamValue::Float(v) => format!("{v}"),
64 ParamValue::Text(s) => s.clone(),
65 ParamValue::IntList(v) => v.iter().map(i64::to_string).collect::<Vec<_>>().join(", "),
66 }
67 }
68
69 fn type_name(&self) -> &'static str {
70 match self {
71 ParamValue::Bool(_) => "bool",
72 ParamValue::Int(_) => "int",
73 ParamValue::Float(_) => "float",
74 ParamValue::Text(_) => "text",
75 ParamValue::IntList(_) => "int-list",
76 }
77 }
78}
79
80impl ParamKind {
81 pub fn parse(&self, raw: &str) -> Result<ParamValue> {
85 let s = raw.trim();
86 match self {
87 ParamKind::Bool => match s.to_ascii_lowercase().as_str() {
88 "true" | "yes" | "on" | "1" => Ok(ParamValue::Bool(true)),
89 "false" | "no" | "off" | "0" => Ok(ParamValue::Bool(false)),
90 _ => bail!("expected true or false, got {s:?}"),
91 },
92 ParamKind::Int { min, max } => {
93 let v: i64 = s.parse().map_err(|_| anyhow!("not an integer: {s:?}"))?;
94 if v < *min || v > *max {
95 bail!("must be between {min} and {max}, got {v}");
96 }
97 Ok(ParamValue::Int(v))
98 }
99 ParamKind::Float { min, max } => {
100 let v: f64 = s.parse().map_err(|_| anyhow!("not a number: {s:?}"))?;
101 if !v.is_finite() {
102 bail!("must be finite, got {s:?}");
103 }
104 if v < *min || v > *max {
105 bail!("must be between {min} and {max}, got {v}");
106 }
107 Ok(ParamValue::Float(v))
108 }
109 ParamKind::Text => {
110 if s.is_empty() {
111 bail!("must not be empty");
112 }
113 Ok(ParamValue::Text(s.to_string()))
114 }
115 ParamKind::Choice(options) => options
116 .iter()
117 .find(|o| o.eq_ignore_ascii_case(s))
118 .map(|o| ParamValue::Text((*o).to_string()))
119 .ok_or_else(|| anyhow!("must be one of: {}", options.join(", "))),
120 ParamKind::IntList { min, max } => {
121 let mut out = Vec::new();
122 for part in s.split(',') {
123 let p = part.trim();
124 if p.is_empty() {
125 continue;
126 }
127 let v: i64 = p.parse().map_err(|_| anyhow!("not an integer: {p:?}"))?;
128 if v < *min || v > *max {
129 bail!("every entry must be between {min} and {max}, got {v}");
130 }
131 out.push(v);
132 }
133 if out.is_empty() {
134 bail!("must list at least one integer");
135 }
136 Ok(ParamValue::IntList(out))
137 }
138 }
139 }
140
141 pub fn domain_hint(&self) -> String {
144 match self {
145 ParamKind::Bool => "true / false".into(),
146 ParamKind::Int { min, max } | ParamKind::IntList { min, max } => {
147 format!("{min}–{max}")
148 }
149 ParamKind::Float { min, max } => format!("{min}–{max}"),
150 ParamKind::Text => "text".into(),
151 ParamKind::Choice(options) => options.join(" / "),
152 }
153 }
154}
155
156#[derive(Clone, Debug)]
158pub struct ParamSpec {
159 pub key: &'static str,
160 pub label: &'static str,
161 pub help: &'static str,
164 pub kind: ParamKind,
165 pub default: ParamValue,
166}
167
168impl ParamSpec {
169 pub fn new(
170 key: &'static str,
171 label: &'static str,
172 help: &'static str,
173 kind: ParamKind,
174 default: ParamValue,
175 ) -> Self {
176 Self {
177 key,
178 label,
179 help,
180 kind,
181 default,
182 }
183 }
184}
185
186#[derive(Clone, Debug, Default, PartialEq)]
193pub struct ParamValues(pub(crate) BTreeMap<String, ParamValue>);
194
195impl ParamValues {
196 pub fn defaults(specs: &[ParamSpec]) -> Self {
198 Self(
199 specs
200 .iter()
201 .map(|s| (s.key.to_string(), s.default.clone()))
202 .collect(),
203 )
204 }
205
206 pub fn set(&mut self, key: impl Into<String>, value: ParamValue) {
207 self.0.insert(key.into(), value);
208 }
209
210 pub fn get(&self, key: &str) -> Option<&ParamValue> {
211 self.0.get(key)
212 }
213
214 fn require(&self, key: &str) -> Result<&ParamValue> {
215 self.0
216 .get(key)
217 .ok_or_else(|| anyhow!("parameter {key:?} was never set"))
218 }
219
220 fn mistyped(key: &str, want: &str, got: &ParamValue) -> anyhow::Error {
221 anyhow!("parameter {key:?} is {}, expected {want}", got.type_name())
222 }
223
224 pub fn bool(&self, key: &str) -> Result<bool> {
225 match self.require(key)? {
226 ParamValue::Bool(b) => Ok(*b),
227 other => Err(Self::mistyped(key, "bool", other)),
228 }
229 }
230
231 pub fn int(&self, key: &str) -> Result<i64> {
232 match self.require(key)? {
233 ParamValue::Int(i) => Ok(*i),
234 other => Err(Self::mistyped(key, "int", other)),
235 }
236 }
237
238 pub fn usize(&self, key: &str) -> Result<usize> {
240 let v = self.int(key)?;
241 usize::try_from(v).map_err(|_| anyhow!("parameter {key:?} must not be negative, got {v}"))
242 }
243
244 pub fn float(&self, key: &str) -> Result<f64> {
245 match self.require(key)? {
246 ParamValue::Float(v) => Ok(*v),
247 ParamValue::Int(i) => Ok(*i as f64),
249 other => Err(Self::mistyped(key, "float", other)),
250 }
251 }
252
253 pub fn text(&self, key: &str) -> Result<&str> {
254 match self.require(key)? {
255 ParamValue::Text(s) => Ok(s.as_str()),
256 other => Err(Self::mistyped(key, "text", other)),
257 }
258 }
259
260 pub fn int_list(&self, key: &str) -> Result<&[i64]> {
261 match self.require(key)? {
262 ParamValue::IntList(v) => Ok(v.as_slice()),
263 other => Err(Self::mistyped(key, "int-list", other)),
264 }
265 }
266
267 pub fn validate_against(&self, specs: &[ParamSpec]) -> Result<()> {
273 for key in self.0.keys() {
274 if !specs.iter().any(|spec| spec.key == key) {
275 let known = specs.iter().map(|spec| spec.key).collect::<Vec<_>>();
276 bail!(
277 "unknown parameter {key:?} — this benchmark takes: {}",
278 known.join(", ")
279 );
280 }
281 }
282 for spec in specs {
283 let value = self.require(spec.key)?;
284 spec.kind
287 .parse(&value.to_edit_string())
288 .map_err(|e| anyhow!("{}: {e}", spec.label))?;
289 }
290 Ok(())
291 }
292}
293
294#[cfg(test)]
295#[path = "params_tests.rs"]
296mod tests;