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}