miden_assembly/library/
mod.rs

1use alloc::{
2    collections::{BTreeMap, BTreeSet},
3    string::String,
4    sync::Arc,
5    vec::Vec,
6};
7
8use vm_core::{
9    crypto::hash::RpoDigest,
10    debuginfo::Span,
11    mast::{MastForest, MastNodeId},
12    utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable},
13    Kernel,
14};
15
16use crate::ast::{Ident, ProcedureName, QualifiedProcedureName};
17
18mod error;
19mod module;
20mod namespace;
21mod path;
22mod version;
23
24pub use module::{ModuleInfo, ProcedureInfo};
25
26pub use self::{
27    error::LibraryError,
28    namespace::{LibraryNamespace, LibraryNamespaceError},
29    path::{LibraryPath, LibraryPathComponent, PathError},
30    version::{Version, VersionError},
31};
32
33#[cfg(test)]
34mod tests;
35
36// LIBRARY
37// ================================================================================================
38
39/// Represents a library where all modules were compiled into a [`MastForest`].
40///
41/// A library exports a set of one or more procedures. Currently, all exported procedures belong
42/// to the same top-level namespace.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Library {
45    /// The content hash of this library, formed by hashing the roots of all exports in
46    /// lexicographical order (by digest, not procedure name)
47    digest: RpoDigest,
48    /// A map between procedure paths and the corresponding procedure roots in the MAST forest.
49    /// Multiple paths can map to the same root, and also, some roots may not be associated with
50    /// any paths.
51    ///
52    /// Note that we use `MastNodeId` as an identifier for procedures instead of MAST root, since 2
53    /// different procedures with the same MAST root can be different due to the decorators they
54    /// contain. However, note that `MastNodeId` is also not a unique identifier for procedures; if
55    /// the procedures have the same MAST root and decorators, they will have the same
56    /// `MastNodeId`.
57    exports: BTreeMap<QualifiedProcedureName, MastNodeId>,
58    /// The MAST forest underlying this library.
59    mast_forest: Arc<MastForest>,
60}
61
62impl AsRef<Library> for Library {
63    #[inline(always)]
64    fn as_ref(&self) -> &Library {
65        self
66    }
67}
68
69// ------------------------------------------------------------------------------------------------
70/// Constructors
71impl Library {
72    /// Constructs a new [`Library`] from the provided MAST forest and a set of exports.
73    ///
74    /// # Errors
75    /// Returns an error if the set of exports is empty.
76    /// Returns an error if any of the specified exports do not have a corresponding procedure root
77    /// in the provided MAST forest.
78    pub fn new(
79        mast_forest: Arc<MastForest>,
80        exports: BTreeMap<QualifiedProcedureName, MastNodeId>,
81    ) -> Result<Self, LibraryError> {
82        if exports.is_empty() {
83            return Err(LibraryError::NoExport);
84        }
85        for (fqn, &proc_body_id) in exports.iter() {
86            if !mast_forest.is_procedure_root(proc_body_id) {
87                return Err(LibraryError::NoProcedureRootForExport { procedure_path: fqn.clone() });
88            }
89        }
90
91        let digest = compute_content_hash(&exports, &mast_forest);
92
93        Ok(Self { digest, exports, mast_forest })
94    }
95}
96
97// ------------------------------------------------------------------------------------------------
98/// Public accessors
99impl Library {
100    /// Returns the [RpoDigest] representing the content hash of this library
101    pub fn digest(&self) -> &RpoDigest {
102        &self.digest
103    }
104
105    /// Returns the fully qualified name of all procedures exported by the library.
106    pub fn exports(&self) -> impl Iterator<Item = &QualifiedProcedureName> {
107        self.exports.keys()
108    }
109
110    /// Returns the number of exports in this library.
111    pub fn num_exports(&self) -> usize {
112        self.exports.len()
113    }
114
115    /// Returns a MAST node ID associated with the specified exported procedure.
116    ///
117    /// # Panics
118    /// Panics if the specified procedure is not exported from this library.
119    pub fn get_export_node_id(&self, proc_name: &QualifiedProcedureName) -> MastNodeId {
120        *self.exports.get(proc_name).expect("procedure not exported from the library")
121    }
122
123    /// Returns true if the specified exported procedure is re-exported from a dependency.
124    pub fn is_reexport(&self, proc_name: &QualifiedProcedureName) -> bool {
125        self.exports
126            .get(proc_name)
127            .map(|&node_id| self.mast_forest[node_id].is_external())
128            .unwrap_or(false)
129    }
130
131    /// Returns a reference to the inner [`MastForest`].
132    pub fn mast_forest(&self) -> &Arc<MastForest> {
133        &self.mast_forest
134    }
135}
136
137/// Conversions
138impl Library {
139    /// Returns an iterator over the module infos of the library.
140    pub fn module_infos(&self) -> impl Iterator<Item = ModuleInfo> {
141        let mut modules_by_path: BTreeMap<LibraryPath, ModuleInfo> = BTreeMap::new();
142
143        for (proc_name, &proc_root_node_id) in self.exports.iter() {
144            modules_by_path
145                .entry(proc_name.module.clone())
146                .and_modify(|compiled_module| {
147                    let proc_digest = self.mast_forest[proc_root_node_id].digest();
148                    compiled_module.add_procedure(proc_name.name.clone(), proc_digest);
149                })
150                .or_insert_with(|| {
151                    let mut module_info = ModuleInfo::new(proc_name.module.clone());
152
153                    let proc_digest = self.mast_forest[proc_root_node_id].digest();
154                    module_info.add_procedure(proc_name.name.clone(), proc_digest);
155
156                    module_info
157                });
158        }
159
160        modules_by_path.into_values()
161    }
162}
163
164impl Serializable for Library {
165    fn write_into<W: ByteWriter>(&self, target: &mut W) {
166        let Self { digest: _, exports, mast_forest } = self;
167
168        mast_forest.write_into(target);
169
170        target.write_usize(exports.len());
171        for (proc_name, proc_node_id) in exports {
172            proc_name.module.write_into(target);
173            proc_name.name.as_str().write_into(target);
174            target.write_u32(proc_node_id.as_u32());
175        }
176    }
177}
178
179impl Deserializable for Library {
180    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
181        let mast_forest = Arc::new(MastForest::read_from(source)?);
182
183        let num_exports = source.read_usize()?;
184        if num_exports == 0 {
185            return Err(DeserializationError::InvalidValue(String::from("No exported procedures")));
186        };
187        let mut exports = BTreeMap::new();
188        for _ in 0..num_exports {
189            let proc_module = source.read()?;
190            let proc_name: String = source.read()?;
191            let proc_name = ProcedureName::new_unchecked(Ident::new_unchecked(Span::unknown(
192                Arc::from(proc_name),
193            )));
194            let proc_name = QualifiedProcedureName::new(proc_module, proc_name);
195            let proc_node_id = MastNodeId::from_u32_safe(source.read_u32()?, &mast_forest)?;
196
197            exports.insert(proc_name, proc_node_id);
198        }
199
200        let digest = compute_content_hash(&exports, &mast_forest);
201
202        Ok(Self { digest, exports, mast_forest })
203    }
204}
205
206fn compute_content_hash(
207    exports: &BTreeMap<QualifiedProcedureName, MastNodeId>,
208    mast_forest: &MastForest,
209) -> RpoDigest {
210    let digests = BTreeSet::from_iter(exports.values().map(|&id| mast_forest[id].digest()));
211    digests
212        .into_iter()
213        .reduce(|a, b| vm_core::crypto::hash::Rpo256::merge(&[a, b]))
214        .unwrap()
215}
216
217#[cfg(feature = "std")]
218mod use_std_library {
219    use std::{fs, io, path::Path};
220
221    use miette::Report;
222    use vm_core::utils::ReadAdapter;
223
224    use super::*;
225    use crate::Assembler;
226
227    impl Library {
228        /// File extension for the Assembly Library.
229        pub const LIBRARY_EXTENSION: &'static str = "masl";
230
231        /// Write the library to a target file
232        ///
233        /// NOTE: It is up to the caller to use the correct file extension, but there is no
234        /// specific requirement that the extension be set, or the same as
235        /// [`Self::LIBRARY_EXTENSION`].
236        pub fn write_to_file(&self, path: impl AsRef<Path>) -> io::Result<()> {
237            let path = path.as_ref();
238
239            if let Some(dir) = path.parent() {
240                fs::create_dir_all(dir)?;
241            }
242
243            // NOTE: We catch panics due to i/o errors here due to the fact that the ByteWriter
244            // trait does not provide fallible APIs, so WriteAdapter will panic if the underlying
245            // writes fail. This needs to be addressed in winterfell at some point
246            std::panic::catch_unwind(|| {
247                let mut file = fs::File::create(path)?;
248                self.write_into(&mut file);
249                Ok(())
250            })
251            .map_err(|p| {
252                match p.downcast::<io::Error>() {
253                    // SAFETY: It is guaranteed safe to read Box<std::io::Error>
254                    Ok(err) => unsafe { core::ptr::read(&*err) },
255                    Err(err) => std::panic::resume_unwind(err),
256                }
257            })?
258        }
259
260        /// Create a [Library] from a standard Miden Assembly project layout.
261        ///
262        /// The standard layout dictates that a given path is the root of a namespace, and the
263        /// directory hierarchy corresponds to the namespace hierarchy. A `.masm` file found in a
264        /// given subdirectory of the root, will be parsed with its [LibraryPath] set based on
265        /// where it resides in the directory structure.
266        ///
267        /// This function recursively parses the entire directory structure under `path`, ignoring
268        /// any files which do not have the `.masm` extension.
269        ///
270        /// For example, let's say I call this function like so:
271        ///
272        /// ```rust
273        /// use std::sync::Arc;
274        ///
275        /// use miden_assembly::{Assembler, Library, LibraryNamespace};
276        /// use vm_core::debuginfo::DefaultSourceManager;
277        ///
278        /// Library::from_dir(
279        ///     "~/masm/std",
280        ///     LibraryNamespace::new("std").unwrap(),
281        ///     Assembler::new(Arc::new(DefaultSourceManager::default())),
282        /// );
283        /// ```
284        ///
285        /// Here's how we would handle various files under this path:
286        ///
287        /// - ~/masm/std/sys.masm            -> Parsed as "std::sys"
288        /// - ~/masm/std/crypto/hash.masm    -> Parsed as "std::crypto::hash"
289        /// - ~/masm/std/math/u32.masm       -> Parsed as "std::math::u32"
290        /// - ~/masm/std/math/u64.masm       -> Parsed as "std::math::u64"
291        /// - ~/masm/std/math/README.md      -> Ignored
292        pub fn from_dir(
293            path: impl AsRef<Path>,
294            namespace: LibraryNamespace,
295            assembler: Assembler,
296        ) -> Result<Self, Report> {
297            let path = path.as_ref();
298
299            let modules =
300                crate::parser::read_modules_from_dir(namespace, path, &assembler.source_manager())?;
301            assembler.assemble_library(modules)
302        }
303
304        pub fn deserialize_from_file(path: impl AsRef<Path>) -> Result<Self, DeserializationError> {
305            let path = path.as_ref();
306            let mut file = fs::File::open(path).map_err(|err| {
307                DeserializationError::InvalidValue(format!(
308                    "failed to open file at {}: {err}",
309                    path.to_string_lossy()
310                ))
311            })?;
312            let mut adapter = ReadAdapter::new(&mut file);
313
314            Self::read_from(&mut adapter)
315        }
316    }
317}
318
319// KERNEL LIBRARY
320// ================================================================================================
321
322/// Represents a library containing a Miden VM kernel.
323///
324/// This differs from the regular [Library] as follows:
325/// - All exported procedures must be exported directly from the kernel namespace (i.e., `#sys`).
326/// - There must be at least one exported procedure.
327/// - The number of exported procedures cannot exceed [Kernel::MAX_NUM_PROCEDURES] (i.e., 256).
328#[derive(Debug, Clone, PartialEq, Eq)]
329pub struct KernelLibrary {
330    kernel: Kernel,
331    kernel_info: ModuleInfo,
332    library: Library,
333}
334
335impl AsRef<Library> for KernelLibrary {
336    #[inline(always)]
337    fn as_ref(&self) -> &Library {
338        &self.library
339    }
340}
341
342impl KernelLibrary {
343    /// Returns the [Kernel] for this kernel library.
344    pub fn kernel(&self) -> &Kernel {
345        &self.kernel
346    }
347
348    /// Returns a reference to the inner [`MastForest`].
349    pub fn mast_forest(&self) -> &Arc<MastForest> {
350        self.library.mast_forest()
351    }
352
353    /// Destructures this kernel library into individual parts.
354    pub fn into_parts(self) -> (Kernel, ModuleInfo, Arc<MastForest>) {
355        (self.kernel, self.kernel_info, self.library.mast_forest)
356    }
357}
358
359impl TryFrom<Library> for KernelLibrary {
360    type Error = LibraryError;
361
362    fn try_from(library: Library) -> Result<Self, Self::Error> {
363        let kernel_path = LibraryPath::from(LibraryNamespace::Kernel);
364        let mut proc_digests = Vec::with_capacity(library.exports.len());
365
366        let mut kernel_module = ModuleInfo::new(kernel_path.clone());
367
368        for (proc_path, &proc_node_id) in library.exports.iter() {
369            // make sure all procedures are exported only from the kernel root
370            if proc_path.module != kernel_path {
371                return Err(LibraryError::InvalidKernelExport {
372                    procedure_path: proc_path.clone(),
373                });
374            }
375
376            let proc_digest = library.mast_forest[proc_node_id].digest();
377            proc_digests.push(proc_digest);
378            kernel_module.add_procedure(proc_path.name.clone(), proc_digest);
379        }
380
381        let kernel = Kernel::new(&proc_digests).map_err(LibraryError::KernelConversion)?;
382
383        Ok(Self {
384            kernel,
385            kernel_info: kernel_module,
386            library,
387        })
388    }
389}
390
391impl Serializable for KernelLibrary {
392    fn write_into<W: ByteWriter>(&self, target: &mut W) {
393        let Self { kernel: _, kernel_info: _, library } = self;
394
395        library.write_into(target);
396    }
397}
398
399impl Deserializable for KernelLibrary {
400    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
401        let library = Library::read_from(source)?;
402
403        Self::try_from(library).map_err(|err| {
404            DeserializationError::InvalidValue(format!(
405                "Failed to deserialize kernel library: {err}"
406            ))
407        })
408    }
409}
410
411#[cfg(feature = "std")]
412mod use_std_kernel {
413    use std::{io, path::Path};
414
415    use super::*;
416    use crate::{diagnostics::Report, Assembler};
417
418    impl KernelLibrary {
419        /// Write the library to a target file
420        pub fn write_to_file(&self, path: impl AsRef<Path>) -> io::Result<()> {
421            self.library.write_to_file(path)
422        }
423
424        /// Create a [KernelLibrary] from a standard Miden Assembly kernel project layout.
425        ///
426        /// The kernel library will export procedures defined by the module at `sys_module_path`.
427        /// If the optional `lib_dir` is provided, all modules under this directory will be
428        /// available from the kernel module under the `kernel` namespace. For example, if
429        /// `lib_dir` is set to "~/masm/lib", the files will be accessible in the kernel module as
430        /// follows:
431        ///
432        /// - ~/masm/lib/foo.masm        -> Can be imported as "kernel::foo"
433        /// - ~/masm/lib/bar/baz.masm    -> Can be imported as "kernel::bar::baz"
434        ///
435        /// Note: this is a temporary structure which will likely change once
436        /// <https://github.com/0xPolygonMiden/miden-vm/issues/1436> is implemented.
437        pub fn from_dir(
438            sys_module_path: impl AsRef<Path>,
439            lib_dir: Option<impl AsRef<Path>>,
440            mut assembler: Assembler,
441        ) -> Result<Self, Report> {
442            // if library directory is provided, add modules from this directory to the assembler
443            if let Some(lib_dir) = lib_dir {
444                let lib_dir = lib_dir.as_ref();
445                let namespace = LibraryNamespace::new("kernel").expect("invalid namespace");
446                assembler.add_modules_from_dir(namespace, lib_dir)?;
447            }
448
449            assembler.assemble_kernel(sys_module_path.as_ref())
450        }
451    }
452}