Skip to main content

miden_debug/
config.rs

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