Skip to main content

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}