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