Skip to main content

miden_debug/
config.rs

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