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