1use std::{ffi::OsStr, path::Path};
2
3use miden_processor::{AdviceInputs, ExecutionOptions, StackInputs};
4use serde::Deserialize;
5
6use crate::felt::Felt;
7
8#[derive(Debug, Clone, Default, Deserialize)]
9#[serde(try_from = "ExecutionConfigFile")]
10pub struct ExecutionConfig {
11 pub inputs: StackInputs,
12 pub advice_inputs: AdviceInputs,
13 pub options: ExecutionOptions,
14}
15
16impl TryFrom<ExecutionConfigFile> for ExecutionConfig {
17 type Error = String;
18
19 #[inline]
20 fn try_from(file: ExecutionConfigFile) -> Result<Self, Self::Error> {
21 Self::from_inputs_file(file)
22 }
23}
24
25impl ExecutionConfig {
26 pub fn parse_file<P>(path: P) -> std::io::Result<Self>
27 where
28 P: AsRef<std::path::Path>,
29 {
30 let path = path.as_ref();
31 let content = std::fs::read_to_string(path)?;
32
33 let file =
34 toml::from_str::<ExecutionConfigFile>(&content).map_err(std::io::Error::other)?;
35 Self::from_inputs_file(file).map_err(std::io::Error::other)
36 }
37
38 pub fn parse_str(content: &str) -> Result<Self, String> {
39 let file = toml::from_str::<ExecutionConfigFile>(content).map_err(|err| err.to_string())?;
40
41 Self::from_inputs_file(file)
42 }
43
44 fn from_inputs_file(file: ExecutionConfigFile) -> Result<Self, String> {
45 let inputs = StackInputs::new(file.inputs.stack.into_iter().map(|felt| felt.0).collect())
46 .map_err(|err| format!("invalid value for 'stack': {err}"))?;
47 let advice_inputs = AdviceInputs::default()
48 .with_stack(file.inputs.advice.stack.into_iter().rev().map(|felt| felt.0))
49 .with_map(file.inputs.advice.map.into_iter().map(|entry| {
50 (entry.digest.0, entry.values.into_iter().map(|felt| felt.0).collect::<Vec<_>>())
51 }));
52
53 Ok(Self {
54 inputs,
55 advice_inputs,
56 options: file.options,
57 })
58 }
59}
60
61#[derive(Debug, Clone, Default, Deserialize)]
62#[serde(default)]
63struct ExecutionConfigFile {
64 inputs: Inputs,
65 #[serde(deserialize_with = "deserialize_execution_options")]
66 options: ExecutionOptions,
67}
68
69#[derive(Debug, Clone, Default, Deserialize)]
70#[serde(default)]
71struct Inputs {
72 stack: Vec<Felt>,
74 advice: Advice,
76}
77
78#[derive(Debug, Clone, Default, Deserialize)]
79#[serde(default)]
80struct Advice {
81 stack: Vec<Felt>,
83 map: Vec<AdviceMapEntry>,
85}
86
87#[derive(Debug, Clone, Deserialize)]
88struct AdviceMapEntry {
89 digest: Word,
90 values: Vec<Felt>,
92}
93
94#[cfg(feature = "tui")]
95impl clap::builder::ValueParserFactory for ExecutionConfig {
96 type Parser = ExecutionConfigParser;
97
98 fn value_parser() -> Self::Parser {
99 ExecutionConfigParser
100 }
101}
102
103#[cfg(feature = "tui")]
104#[doc(hidden)]
105#[derive(Clone)]
106pub struct ExecutionConfigParser;
107
108#[cfg(feature = "tui")]
109impl clap::builder::TypedValueParser for ExecutionConfigParser {
110 type Value = ExecutionConfig;
111
112 fn parse_ref(
113 &self,
114 _cmd: &clap::Command,
115 _arg: Option<&clap::Arg>,
116 value: &OsStr,
117 ) -> Result<Self::Value, clap::error::Error> {
118 use clap::error::{Error, ErrorKind};
119
120 let inputs_path = Path::new(value);
121 if !inputs_path.is_file() {
122 return Err(Error::raw(
123 ErrorKind::InvalidValue,
124 format!("invalid inputs file: '{}' is not a file", inputs_path.display()),
125 ));
126 }
127
128 let content = std::fs::read_to_string(inputs_path).map_err(|err| {
129 Error::raw(ErrorKind::ValueValidation, format!("failed to read inputs file: {err}"))
130 })?;
131 let inputs_file = toml::from_str::<ExecutionConfigFile>(&content).map_err(|err| {
132 Error::raw(ErrorKind::ValueValidation, format!("invalid inputs file: {err}"))
133 })?;
134
135 ExecutionConfig::from_inputs_file(inputs_file).map_err(|err| {
136 Error::raw(ErrorKind::ValueValidation, format!("invalid inputs file: {err}"))
137 })
138 }
139}
140
141#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
142struct Word(miden_core::Word);
143impl<'de> Deserialize<'de> for Word {
144 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
145 where
146 D: serde::Deserializer<'de>,
147 {
148 let digest = String::deserialize(deserializer)?;
149 miden_core::Word::try_from(&digest)
150 .map_err(|err| serde::de::Error::custom(format!("invalid digest: {err}")))
151 .map(Self)
152 }
153}
154
155fn deserialize_execution_options<'de, D>(deserializer: D) -> Result<ExecutionOptions, D::Error>
156where
157 D: serde::Deserializer<'de>,
158{
159 #[derive(Default, Deserialize)]
160 #[serde(default)]
161 struct ExecOptions {
162 max_cycles: Option<u32>,
163 expected_cycles: u32,
164 }
165
166 ExecOptions::deserialize(deserializer).and_then(|opts| {
167 ExecutionOptions::new(
168 opts.max_cycles,
169 opts.expected_cycles,
170 true,
171 true,
172 )
173 .map(|exec_opts| exec_opts.with_debugging(true))
174 .map_err(|err| serde::de::Error::custom(format!("invalid execution options: {err}")))
175 })
176}
177
178#[cfg(test)]
179mod tests {
180 use miden_processor::Felt as RawFelt;
181 use toml::toml;
182
183 use super::*;
184
185 #[test]
186 fn execution_config_empty() {
187 let text = toml::to_string_pretty(&toml! {
188 [inputs]
189 [options]
190 })
191 .unwrap();
192
193 let file = toml::from_str::<ExecutionConfig>(&text).unwrap();
194 let expected_inputs = StackInputs::new(vec![]).unwrap();
195 assert_eq!(file.inputs.as_ref(), expected_inputs.as_ref());
196 assert!(file.advice_inputs.stack.is_empty());
197 assert!(file.options.enable_tracing());
198 assert!(file.options.enable_debugging());
199 assert_eq!(file.options.max_cycles(), ExecutionOptions::MAX_CYCLES);
200 assert_eq!(file.options.expected_cycles(), 64);
201 }
202
203 #[test]
204 fn execution_config_with_options() {
205 let text = toml::to_string_pretty(&toml! {
206 [inputs]
207 [options]
208 max_cycles = 1000
209 })
210 .unwrap();
211
212 let file = ExecutionConfig::parse_str(&text).unwrap();
213 let expected_inputs = StackInputs::new(vec![]).unwrap();
214 assert_eq!(file.inputs.as_ref(), expected_inputs.as_ref());
215 assert!(file.advice_inputs.stack.is_empty());
216 assert!(file.options.enable_tracing());
217 assert!(file.options.enable_debugging());
218 assert_eq!(file.options.max_cycles(), 1000);
219 assert_eq!(file.options.expected_cycles(), 64);
220 }
221
222 #[test]
223 fn execution_config_with_operands() {
224 let text = toml::to_string_pretty(&toml! {
225 [inputs]
226 stack = [1, 2, 3]
227
228 [options]
229 max_cycles = 1000
230 })
231 .unwrap();
232
233 let file = ExecutionConfig::parse_str(&text).unwrap();
234 let expected_inputs =
235 StackInputs::new(vec![RawFelt::new(1), RawFelt::new(2), RawFelt::new(3)]).unwrap();
236 assert_eq!(file.inputs.as_ref(), expected_inputs.as_ref());
237 assert!(file.advice_inputs.stack.is_empty());
238 assert!(file.options.enable_tracing());
239 assert!(file.options.enable_debugging());
240 assert_eq!(file.options.max_cycles(), 1000);
241 assert_eq!(file.options.expected_cycles(), 64);
242 }
243
244 #[test]
245 fn execution_config_with_advice() {
246 let text = toml::to_string_pretty(&toml! {
247 [inputs]
248 stack = [1, 2, 0x3]
249
250 [inputs.advice]
251 stack = [1, 2, 3, 4]
252
253 [[inputs.advice.map]]
254 digest = "0x3cff5b58a573dc9d25fd3c57130cc57e5b1b381dc58b5ae3594b390c59835e63"
255 values = [1, 2, 3, 4]
256
257 [options]
258 max_cycles = 1000
259 })
260 .unwrap();
261 let digest = miden_core::Word::try_from(
262 "0x3cff5b58a573dc9d25fd3c57130cc57e5b1b381dc58b5ae3594b390c59835e63",
263 )
264 .unwrap();
265 let file = ExecutionConfig::parse_str(&text).unwrap_or_else(|err| panic!("{err}"));
266 let expected_inputs =
267 StackInputs::new(vec![RawFelt::new(1), RawFelt::new(2), RawFelt::new(3)]).unwrap();
268 assert_eq!(file.inputs.as_ref(), expected_inputs.as_ref());
269 assert_eq!(
270 file.advice_inputs.stack,
271 &[RawFelt::new(4), RawFelt::new(3), RawFelt::new(2), RawFelt::new(1)]
272 );
273 assert_eq!(
274 file.advice_inputs.map.get(&digest).map(|value| value.as_ref()),
275 Some([RawFelt::new(1), RawFelt::new(2), RawFelt::new(3), RawFelt::new(4)].as_slice())
276 );
277 assert!(file.options.enable_tracing());
278 assert!(file.options.enable_debugging());
279 assert_eq!(file.options.max_cycles(), 1000);
280 assert_eq!(file.options.expected_cycles(), 64);
281 }
282}