Skip to main content

miden_debug/
linker.rs

1use std::{
2    borrow::Cow,
3    fmt,
4    path::{Path as FsPath, PathBuf},
5    str::FromStr,
6    sync::Arc,
7};
8
9use miden_assembly::SourceManager;
10use miden_assembly_syntax::{
11    Library, Path as LibraryPath,
12    diagnostics::{IntoDiagnostic, Report},
13};
14
15use crate::config::DebuggerConfig;
16
17/// A library requested by the user to be linked against during compilation
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct LinkLibrary {
20    /// The name of the library.
21    ///
22    /// If requested by name, e.g. `-l std`, the name is used as given.
23    ///
24    /// If requested by path, e.g. `-l ./target/libs/miden-base.masl`, then the name of the library
25    /// will be the basename of the file specified in the path.
26    pub name: Cow<'static, str>,
27    /// If specified, the path from which this library should be loaded
28    pub path: Option<PathBuf>,
29    /// The kind of library to load.
30    ///
31    /// By default this is assumed to be a `.masp` package, but the kind will be detected based on
32    /// how it is requested by the user. It may also be specified explicitly by the user.
33    pub kind: LibraryKind,
34}
35
36/// The types of libraries that can be linked against during compilation
37#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
38pub enum LibraryKind {
39    /// A Miden package (MASP)
40    #[default]
41    Masp,
42    /// A source-form MASM library, using the standard project layout
43    Masm,
44}
45impl fmt::Display for LibraryKind {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            Self::Masm => f.write_str("masm"),
49            Self::Masp => f.write_str("masp"),
50        }
51    }
52}
53impl FromStr for LibraryKind {
54    type Err = ();
55
56    fn from_str(s: &str) -> Result<Self, Self::Err> {
57        match s {
58            "masm" => Ok(Self::Masm),
59            "masp" => Ok(Self::Masp),
60            _ => Err(()),
61        }
62    }
63}
64
65impl LinkLibrary {
66    /// Get the name of this library
67    pub fn name(&self) -> &str {
68        self.name.as_ref()
69    }
70
71    pub fn load(
72        &self,
73        config: &DebuggerConfig,
74        source_manager: Arc<dyn SourceManager>,
75    ) -> Result<Arc<Library>, Report> {
76        if let Some(path) = self.path.as_deref() {
77            return self.load_from_path(path, source_manager);
78        }
79
80        // Search for library among specified search paths
81        let path = self.find(config)?;
82
83        self.load_from_path(&path, source_manager)
84    }
85
86    fn load_from_path(
87        &self,
88        path: &FsPath,
89        source_manager: Arc<dyn SourceManager>,
90    ) -> Result<Arc<Library>, Report> {
91        match self.kind {
92            LibraryKind::Masm => {
93                let ns = LibraryPath::validate(self.name.as_ref()).map_err(|err| {
94                    Report::msg(format!("invalid library namespace '{}': {err}", &self.name))
95                })?;
96
97                let modules = miden_assembly_syntax::parser::read_modules_from_dir(
98                    path,
99                    ns,
100                    source_manager.clone(),
101                )?;
102
103                miden_assembly::Assembler::new(source_manager)
104                    .assemble_library(modules)
105                    .map(Arc::new)
106            }
107            LibraryKind::Masp => {
108                use miden_core::utils::Deserializable;
109                let bytes = std::fs::read(path).into_diagnostic()?;
110                let package =
111                    miden_mast_package::Package::read_from_bytes(&bytes).map_err(|e| {
112                        Report::msg(format!(
113                            "failed to load Miden package from {}: {e}",
114                            path.display()
115                        ))
116                    })?;
117                let lib = match package.mast {
118                    miden_mast_package::MastArtifact::Executable(_) => {
119                        return Err(Report::msg(format!(
120                            "Expected Miden package to contain a Library, got Program: '{}'",
121                            path.display()
122                        )));
123                    }
124                    miden_mast_package::MastArtifact::Library(lib) => lib.clone(),
125                };
126                Ok(lib)
127            }
128        }
129    }
130
131    fn find(&self, config: &DebuggerConfig) -> Result<PathBuf, Report> {
132        use std::fs;
133
134        let toolchain_dir = config.toolchain_dir();
135        let search_paths = toolchain_dir
136            .iter()
137            .chain(config.search_path.iter())
138            .chain(config.working_dir.iter());
139
140        for search_path in search_paths {
141            let reader = fs::read_dir(search_path).map_err(|err| {
142                Report::msg(format!(
143                    "invalid library search path '{}': {err}",
144                    search_path.display()
145                ))
146            })?;
147            for entry in reader {
148                let Ok(entry) = entry else {
149                    continue;
150                };
151                let path = entry.path();
152                let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
153                    continue;
154                };
155                if stem != self.name.as_ref() {
156                    continue;
157                }
158
159                match self.kind {
160                    LibraryKind::Masp => {
161                        if !path.is_file() {
162                            return Err(Report::msg(format!(
163                                "unable to load Miden Assembly package from '{}': not a file",
164                                path.display()
165                            )));
166                        }
167                    }
168                    LibraryKind::Masm => {
169                        if !path.is_dir() {
170                            return Err(Report::msg(format!(
171                                "unable to load Miden Assembly library from '{}': not a directory",
172                                path.display()
173                            )));
174                        }
175                    }
176                }
177                return Ok(path);
178            }
179        }
180
181        Err(Report::msg(format!(
182            "unable to locate library '{}' using any of the provided search paths",
183            &self.name
184        )))
185    }
186}
187
188#[cfg(feature = "tui")]
189impl clap::builder::ValueParserFactory for LinkLibrary {
190    type Parser = LinkLibraryParser;
191
192    fn value_parser() -> Self::Parser {
193        LinkLibraryParser
194    }
195}
196
197#[cfg(feature = "tui")]
198#[doc(hidden)]
199#[derive(Clone)]
200pub struct LinkLibraryParser;
201
202#[cfg(feature = "tui")]
203impl clap::builder::TypedValueParser for LinkLibraryParser {
204    type Value = LinkLibrary;
205
206    fn possible_values(
207        &self,
208    ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
209        use clap::builder::PossibleValue;
210
211        Some(Box::new(
212            [
213                PossibleValue::new("masm").help("A Miden Assembly project directory"),
214                PossibleValue::new("masp").help("A compiled Miden package file"),
215            ]
216            .into_iter(),
217        ))
218    }
219
220    /// Parses the `-l` flag using the following format:
221    ///
222    /// `-l[KIND=]NAME`
223    ///
224    /// * `KIND` is one of: `masp`, `masm`; defaults to `masp`
225    /// * `NAME` is either an absolute path, or a name (without extension)
226    fn parse_ref(
227        &self,
228        _cmd: &clap::Command,
229        _arg: Option<&clap::Arg>,
230        value: &std::ffi::OsStr,
231    ) -> Result<Self::Value, clap::error::Error> {
232        use clap::error::{Error, ErrorKind};
233
234        let value = value.to_str().ok_or_else(|| Error::new(ErrorKind::InvalidUtf8))?;
235        let (kind, name) = value
236            .split_once('=')
237            .map(|(kind, name)| (Some(kind), name))
238            .unwrap_or((None, value));
239
240        if name.is_empty() {
241            return Err(Error::raw(
242                ErrorKind::ValueValidation,
243                "invalid link library: must specify a name or path",
244            ));
245        }
246
247        let maybe_path = FsPath::new(name);
248        let extension = maybe_path.extension().map(|ext| ext.to_str().unwrap());
249        let kind = match kind {
250            Some(kind) if !kind.is_empty() => kind.parse::<LibraryKind>().map_err(|_| {
251                Error::raw(ErrorKind::InvalidValue, format!("'{kind}' is not a valid library kind"))
252            })?,
253            Some(_) | None => match extension {
254                Some(kind) => kind.parse::<LibraryKind>().map_err(|_| {
255                    Error::raw(
256                        ErrorKind::InvalidValue,
257                        format!("'{kind}' is not a valid library kind"),
258                    )
259                })?,
260                None => LibraryKind::default(),
261            },
262        };
263
264        if maybe_path.is_absolute() {
265            let meta = maybe_path.metadata().map_err(|err| {
266                Error::raw(
267                    ErrorKind::ValueValidation,
268                    format!(
269                        "invalid link library: unable to load '{}': {err}",
270                        maybe_path.display()
271                    ),
272                )
273            })?;
274
275            match kind {
276                LibraryKind::Masp if !meta.is_file() => {
277                    return Err(Error::raw(
278                        ErrorKind::ValueValidation,
279                        format!("invalid link library: '{}' is not a file", maybe_path.display()),
280                    ));
281                }
282                LibraryKind::Masm if !meta.is_dir() => {
283                    return Err(Error::raw(
284                        ErrorKind::ValueValidation,
285                        format!(
286                            "invalid link library: kind 'masm' was specified, but '{}' is not a \
287                             directory",
288                            maybe_path.display()
289                        ),
290                    ));
291                }
292                _ => (),
293            }
294
295            let name = maybe_path.file_stem().unwrap().to_str().unwrap().to_string();
296
297            Ok(LinkLibrary {
298                name: name.into(),
299                path: Some(maybe_path.to_path_buf()),
300                kind,
301            })
302        } else if extension.is_some() {
303            let name = name.strip_suffix(unsafe { extension.unwrap_unchecked() }).unwrap();
304            let mut name = name.to_string();
305            name.pop();
306
307            Ok(LinkLibrary {
308                name: name.into(),
309                path: None,
310                kind,
311            })
312        } else {
313            Ok(LinkLibrary {
314                name: name.to_string().into(),
315                path: None,
316                kind,
317            })
318        }
319    }
320}