midenc_session/
libs.rs

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