midenc_session/
libs.rs

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