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