miden_debug/config.rs
1use std::{
2 borrow::Cow,
3 path::{Path, PathBuf},
4 str::FromStr,
5};
6
7use miden_debug_engine::{LinkLibrary, profiling::ProfilerCliArgs};
8
9use crate::{exec::ExecutionConfig, felt::Felt, input::InputFile};
10
11/// Run a compiled Miden package with the Miden VM
12#[derive(Default, Debug)]
13#[cfg_attr(
14 any(feature = "tui", feature = "repl", feature = "flamegraph"),
15 derive(clap::Args)
16)]
17pub struct DebuggerConfig {
18 /// Specify the path to a Miden package artifact to execute.
19 ///
20 /// Miden Assembly packages are emitted by the compiler with a `.masp` extension.
21 ///
22 /// You may use `-` as a file name to read a file from stdin.
23 #[cfg_attr(
24 any(feature = "tui", feature = "repl", feature = "flamegraph"),
25 arg(value_name = "FILE")
26 )]
27 pub input: Option<InputFile>,
28 /// Specify the path to a file containing program inputs.
29 ///
30 /// Program inputs are stack and advice provider values which the program can
31 /// access during execution. The inputs file is a TOML file which describes
32 /// what the inputs are, or where to source them from.
33 #[cfg_attr(
34 any(feature = "tui", feature = "repl", feature = "flamegraph"),
35 arg(long, value_name = "FILE")
36 )]
37 pub inputs: Option<ExecutionConfig>,
38 /// Arguments to place on the operand stack before calling the program entrypoint.
39 ///
40 /// Arguments will be pushed on the operand stack in the order of appearance,
41 ///
42 /// Example: `-- a b` will push `a` on the stack, then `b`.
43 ///
44 /// These arguments must be valid field element values expressed in decimal format.
45 ///
46 /// NOTE: These arguments will override any stack values provided via --inputs
47 #[cfg_attr(
48 any(feature = "tui", feature = "repl", feature = "flamegraph"),
49 arg(last(true), value_name = "ARGV")
50 )]
51 pub args: Vec<Felt>,
52 /// The working directory for the debugger
53 ///
54 /// By default this will be the working directory the debugger is executed from
55 #[cfg_attr(
56 any(feature = "tui", feature = "flamegraph"),
57 arg(long, value_name = "DIR", help_heading = "Execution")
58 )]
59 pub working_dir: Option<PathBuf>,
60 /// The path to the root directory of the current Miden toolchain
61 ///
62 /// By default this is assumed to be `$(midenup show home)/toolchains/$(midenup show active-toolchain)
63 #[cfg_attr(
64 any(feature = "tui", feature = "flamegraph"),
65 arg(
66 long,
67 value_name = "DIR",
68 env = "MIDEN_SYSROOT",
69 help_heading = "Linker"
70 )
71 )]
72 pub sysroot: Option<PathBuf>,
73 /// Whether, and how, to color terminal output
74 #[cfg_attr(any(feature = "tui", feature = "repl", feature = "flamegraph"), arg(
75 long,
76 value_enum,
77 default_value_t = ColorChoice::Auto,
78 default_missing_value = "auto",
79 num_args(0..=1),
80 help_heading = "Output"
81 ))]
82 pub color: ColorChoice,
83 /// Specify the function to call as the entrypoint for the program
84 /// in the format `<module_name>::<function>`
85 #[cfg_attr(
86 any(feature = "tui", feature = "repl", feature = "flamegraph"),
87 arg(long, help_heading = "Execution")
88 )]
89 pub entrypoint: Option<String>,
90 /// Connect to a remote DAP debug server instead of running a local program.
91 ///
92 /// Specify the address of the DAP server (e.g. "127.0.0.1:4711").
93 /// When this flag is set, the debugger connects to an existing remote session.
94 #[cfg(feature = "dap")]
95 #[cfg_attr(
96 any(feature = "tui", feature = "flamegraph"),
97 arg(long, value_name = "ADDR", help_heading = "Execution")
98 )]
99 pub dap_connect: Option<String>,
100 /// Start a DAP debug server for the local program and wait for a client to connect.
101 ///
102 /// Specify the address to listen on (e.g. "127.0.0.1:4711").
103 #[cfg(feature = "dap")]
104 #[cfg_attr(
105 feature = "tui",
106 arg(long, value_name = "ADDR", help_heading = "Execution")
107 )]
108 pub start_debug_adapter: Option<String>,
109 /// Source path prefixes used by the compiler's `-Zremap-path-prefix` option.
110 ///
111 /// When debug info stores trimmed source paths, DAP clients may still send
112 /// absolute editor paths. These prefixes provide an explicit mapping between
113 /// the two forms.
114 #[cfg(feature = "dap")]
115 #[cfg_attr(
116 feature = "tui",
117 arg(
118 long = "source-path-prefix",
119 alias = "trim-path-prefix",
120 value_name = "PATH",
121 help_heading = "Debugging"
122 )
123 )]
124 pub source_path_prefixes: Vec<PathBuf>,
125 /// Replay a recorded execution snapshot in the TUI debugger.
126 ///
127 /// FILE is a snapshot written during a recorded debug session (e.g.
128 /// `miden-client exec --start-debug-adapter <ADDR> --record <FILE>`). The recorded program,
129 /// inputs, resolved code, and event log are replayed so the same execution can be stepped
130 /// through offline, without the original host.
131 #[cfg_attr(
132 feature = "tui",
133 arg(long, value_name = "FILE", help_heading = "Execution")
134 )]
135 pub replay: Option<PathBuf>,
136 /// Specify one or more search paths for link libraries requested via `-l`
137 #[cfg_attr(
138 any(feature = "tui", feature = "flamegraph"),
139 arg(
140 long = "search-path",
141 short = 'L',
142 value_name = "PATH",
143 help_heading = "Linker"
144 )
145 )]
146 pub search_path: Vec<PathBuf>,
147 /// Link compiled projects to the specified library NAME.
148 ///
149 /// The optional KIND can be provided to indicate what type of library it is.
150 ///
151 /// NAME must either be an absolute path (with extension when applicable), or
152 /// a library namespace (no extension). The former will be used as the path
153 /// to load the library, without looking for it in the library search paths,
154 /// while the latter will be located in the search path based on its KIND.
155 ///
156 /// See below for valid KINDs:
157 #[cfg_attr(
158 any(feature = "tui", feature = "flamegraph"),
159 arg(
160 long = "link-library",
161 short = 'l',
162 value_name = "[KIND=]NAME",
163 value_delimiter = ',',
164 next_line_help(true),
165 help_heading = "Linker"
166 )
167 )]
168 pub link_libraries: Vec<LinkLibrary>,
169 /// Use the REPL (text-mode) debugger instead of the TUI
170 #[cfg_attr(
171 any(feature = "tui", feature = "repl", feature = "flamegraph"),
172 arg(long, help_heading = "Output")
173 )]
174 pub repl: bool,
175 /// Run a script of debugger commands non-interactively, then exit.
176 ///
177 /// FILE is a list of debugger commands, one per line, using the same syntax
178 /// as the interactive REPL prompt. Blank lines and lines beginning with `#`
179 /// are ignored, so scripts may be commented. This is analogous to
180 /// `gdb -x <file> -batch` or `lldb -s <file>`, and is primarily used to
181 /// drive the debugger from lit/FileCheck tests.
182 #[cfg_attr(
183 any(feature = "tui", feature = "repl", feature = "flamegraph"),
184 arg(
185 long = "commands",
186 visible_alias = "source",
187 short = 'x',
188 value_name = "FILE",
189 help_heading = "Execution"
190 )
191 )]
192 pub commands: Option<PathBuf>,
193 /// Do not auto-load the project-local `.miden-debug.py` file.
194 #[cfg(feature = "python")]
195 #[cfg_attr(feature = "python", arg(long, help_heading = "Scripting"))]
196 pub no_user_python_init: bool,
197 /// Profiler configuration.
198 #[cfg_attr(
199 any(feature = "tui", feature = "repl", feature = "flamegraph"),
200 command(flatten)
201 )]
202 pub profiler_cli_args: ProfilerCliArgs,
203}
204
205/// ColorChoice represents the color preferences of an end user.
206///
207/// The `Default` implementation for this type will select `Auto`, which tries
208/// to do the right thing based on the current environment.
209///
210/// The `FromStr` implementation for this type converts a lowercase kebab-case
211/// string of the variant name to the corresponding variant. Any other string
212/// results in an error.
213#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)]
214#[cfg_attr(
215 any(feature = "tui", feature = "repl", feature = "flamegraph"),
216 derive(clap::ValueEnum)
217)]
218pub enum ColorChoice {
219 /// Try very hard to emit colors. This includes emitting ANSI colors
220 /// on Windows if the console API is unavailable.
221 Always,
222 /// AlwaysAnsi is like Always, except it never tries to use anything other
223 /// than emitting ANSI color codes.
224 AlwaysAnsi,
225 /// Try to use colors, but don't force the issue. If the console isn't
226 /// available on Windows, or if TERM=dumb, or if `NO_COLOR` is defined, for
227 /// example, then don't use colors.
228 #[default]
229 Auto,
230 /// Never emit colors.
231 Never,
232}
233
234#[derive(Debug, thiserror::Error)]
235#[error("invalid color choice: {0}")]
236pub struct ColorChoiceParseError(std::borrow::Cow<'static, str>);
237
238impl FromStr for ColorChoice {
239 type Err = ColorChoiceParseError;
240
241 fn from_str(s: &str) -> Result<Self, Self::Err> {
242 match s.to_lowercase().as_str() {
243 "always" => Ok(ColorChoice::Always),
244 "always-ansi" => Ok(ColorChoice::AlwaysAnsi),
245 "never" => Ok(ColorChoice::Never),
246 "auto" => Ok(ColorChoice::Auto),
247 unknown => Err(ColorChoiceParseError(unknown.to_string().into())),
248 }
249 }
250}
251
252impl ColorChoice {
253 /// Returns true if we should attempt to write colored output.
254 pub fn should_attempt_color(&self) -> bool {
255 match *self {
256 ColorChoice::Always => true,
257 ColorChoice::AlwaysAnsi => true,
258 ColorChoice::Never => false,
259 #[cfg(feature = "std")]
260 ColorChoice::Auto => self.env_allows_color(),
261 #[cfg(not(feature = "std"))]
262 ColorChoice::Auto => false,
263 }
264 }
265
266 #[cfg(not(windows))]
267 pub fn env_allows_color(&self) -> bool {
268 match std::env::var_os("TERM") {
269 // If TERM isn't set, then we are in a weird environment that
270 // probably doesn't support colors.
271 None => return false,
272 Some(k) => {
273 if k == "dumb" {
274 return false;
275 }
276 }
277 }
278 // If TERM != dumb, then the only way we don't allow colors at this
279 // point is if NO_COLOR is set.
280 if std::env::var_os("NO_COLOR").is_some() {
281 return false;
282 }
283 true
284 }
285
286 #[cfg(windows)]
287 pub fn env_allows_color(&self) -> bool {
288 // On Windows, if TERM isn't set, then we shouldn't automatically
289 // assume that colors aren't allowed. This is unlike Unix environments
290 // where TERM is more rigorously set.
291 if let Some(k) = std::env::var_os("TERM") {
292 if k == "dumb" {
293 return false;
294 }
295 }
296 // If TERM != dumb, then the only way we don't allow colors at this
297 // point is if NO_COLOR is set.
298 if std::env::var_os("NO_COLOR").is_some() {
299 return false;
300 }
301 true
302 }
303
304 /// Returns true if this choice should forcefully use ANSI color codes.
305 ///
306 /// It's possible that ANSI is still the correct choice even if this
307 /// returns false.
308 #[cfg(all(feature = "tui", windows))]
309 pub fn should_ansi(&self) -> bool {
310 match *self {
311 ColorChoice::Always => false,
312 ColorChoice::AlwaysAnsi => true,
313 ColorChoice::Never => false,
314 ColorChoice::Auto => {
315 match std::env::var("TERM") {
316 Err(_) => false,
317 // cygwin doesn't seem to support ANSI escape sequences
318 // and instead has its own variety. However, the Windows
319 // console API may be available.
320 Ok(k) => k != "dumb" && k != "cygwin",
321 }
322 }
323 }
324 }
325
326 /// Returns true if this choice should forcefully use ANSI color codes.
327 ///
328 /// It's possible that ANSI is still the correct choice even if this
329 /// returns false.
330 #[cfg(not(feature = "tui"))]
331 pub fn should_ansi(&self) -> bool {
332 match *self {
333 ColorChoice::Always => false,
334 ColorChoice::AlwaysAnsi => true,
335 ColorChoice::Never => false,
336 ColorChoice::Auto => false,
337 }
338 }
339}
340
341impl DebuggerConfig {
342 pub fn working_dir(&self) -> Cow<'_, Path> {
343 match self.working_dir.as_deref() {
344 Some(path) => Cow::Borrowed(path),
345 None => std::env::current_dir()
346 .map(Cow::Owned)
347 .unwrap_or(Cow::Borrowed(Path::new("./"))),
348 }
349 }
350
351 pub fn toolchain_dir(&self) -> Option<PathBuf> {
352 let sysroot = if let Some(sysroot) = self.sysroot.as_deref() {
353 Cow::Borrowed(sysroot)
354 } else if let Some((midenup_home, midenup_channel)) = midenup_home().zip(midenup_channel())
355 {
356 Cow::Owned(midenup_home.join("toolchains").join(midenup_channel))
357 } else {
358 return None;
359 };
360
361 if sysroot.try_exists().ok().is_some_and(|exists| exists) {
362 Some(sysroot.into_owned())
363 } else {
364 None
365 }
366 }
367}
368
369fn midenup_home() -> Option<PathBuf> {
370 use std::process::Command;
371
372 let mut cmd = Command::new("midenup");
373 let mut output = cmd.args(["show", "home"]).output().ok()?;
374 if !output.status.success() {
375 return None;
376 }
377 let output = String::from_utf8(core::mem::take(&mut output.stdout)).ok()?;
378 let trimmed = output.trim_ascii();
379 if trimmed.is_empty() {
380 return None;
381 }
382 PathBuf::from_str(trimmed).ok()
383}
384
385fn midenup_channel() -> Option<String> {
386 use std::process::Command;
387
388 let mut cmd = Command::new("midenup");
389 let mut output = cmd.args(["show", "active-toolchain"]).output().ok()?;
390 if !output.status.success() {
391 return None;
392 }
393 let output = String::from_utf8(core::mem::take(&mut output.stdout)).ok()?;
394 let trimmed = output.trim_ascii();
395 if trimmed.is_empty() {
396 return None;
397 }
398 if output.len() == trimmed.len() {
399 Some(output)
400 } else {
401 Some(trimmed.to_string())
402 }
403}