Skip to main content

miden_assembly_syntax/library/
mod.rs

1use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec};
2
3use miden_core::{
4    Word,
5    advice::AdviceMap,
6    mast::{MastForest, MastNodeExt, MastNodeId, UntrustedMastForest},
7    program::Kernel,
8    serde::{
9        ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable, SliceReader,
10    },
11};
12use midenc_hir_type::{FunctionType, Type};
13#[cfg(feature = "arbitrary")]
14use proptest::prelude::*;
15#[cfg(feature = "serde")]
16use serde::{Deserialize, Serialize};
17
18#[cfg(feature = "arbitrary")]
19use crate::ast::QualifiedProcedureName;
20#[cfg(feature = "serde")]
21use crate::ast::path;
22use crate::ast::{AttributeSet, ConstantValue, Ident, Path, PathBuf, PathComponent, ProcedureName};
23
24mod error;
25mod module;
26
27pub use module::{ConstantInfo, ItemInfo, ModuleInfo, ProcedureInfo, TypeInfo};
28pub use semver::{Error as VersionError, Version};
29
30pub use self::error::LibraryError;
31
32// LIBRARY EXPORT
33// ================================================================================================
34
35/// Metadata about a procedure exported by the interface of a [Library]
36#[derive(Debug, Clone, PartialEq, Eq)]
37#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
38#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
39pub enum LibraryExport {
40    Procedure(ProcedureExport),
41    Constant(ConstantExport),
42    Type(TypeExport),
43}
44
45impl LibraryExport {
46    pub fn path(&self) -> Arc<Path> {
47        match self {
48            Self::Procedure(export) => export.path.clone(),
49            Self::Constant(export) => export.path.clone(),
50            Self::Type(export) => export.path.clone(),
51        }
52    }
53
54    pub fn as_procedure(&self) -> Option<&ProcedureExport> {
55        match self {
56            Self::Procedure(proc) => Some(proc),
57            Self::Constant(_) | Self::Type(_) => None,
58        }
59    }
60
61    pub fn unwrap_procedure(&self) -> &ProcedureExport {
62        match self {
63            Self::Procedure(proc) => proc,
64            Self::Constant(_) | Self::Type(_) => panic!("expected export to be a procedure"),
65        }
66    }
67}
68
69impl From<ProcedureExport> for LibraryExport {
70    fn from(value: ProcedureExport) -> Self {
71        Self::Procedure(value)
72    }
73}
74
75impl From<ConstantExport> for LibraryExport {
76    fn from(value: ConstantExport) -> Self {
77        Self::Constant(value)
78    }
79}
80
81impl From<TypeExport> for LibraryExport {
82    fn from(value: TypeExport) -> Self {
83        Self::Type(value)
84    }
85}
86
87#[cfg(feature = "arbitrary")]
88impl Arbitrary for LibraryExport {
89    type Parameters = ();
90
91    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
92        use proptest::{arbitrary::any, prop_oneof, strategy::Strategy};
93
94        prop_oneof![
95            any::<ProcedureExport>().prop_map(Self::Procedure),
96            any::<ConstantExport>().prop_map(Self::Constant),
97            any::<TypeExport>().prop_map(Self::Type),
98        ]
99        .boxed()
100    }
101
102    type Strategy = BoxedStrategy<Self>;
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
107#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
108pub struct ProcedureExport {
109    /// The id of the MAST root node of the exported procedure
110    pub node: MastNodeId,
111    /// The fully-qualified path of the exported procedure
112    #[cfg_attr(feature = "serde", serde(with = "path"))]
113    pub path: Arc<Path>,
114    /// The type signature of the exported procedure, if known
115    #[cfg_attr(feature = "serde", serde(default))]
116    pub signature: Option<FunctionType>,
117    #[cfg_attr(feature = "serde", serde(default))]
118    pub attributes: AttributeSet,
119}
120
121impl ProcedureExport {
122    /// Create a new [ProcedureExport] representing the export of `node` with `path`
123    pub fn new(node: MastNodeId, path: Arc<Path>) -> Self {
124        Self {
125            node,
126            path,
127            signature: None,
128            attributes: Default::default(),
129        }
130    }
131
132    /// Specify the type signature and ABI of this export
133    pub fn with_signature(mut self, signature: FunctionType) -> Self {
134        self.signature = Some(signature);
135        self
136    }
137
138    /// Specify the set of attributes attached to this export
139    pub fn with_attributes(mut self, attrs: AttributeSet) -> Self {
140        self.attributes = attrs;
141        self
142    }
143}
144
145#[cfg(feature = "arbitrary")]
146impl Arbitrary for ProcedureExport {
147    type Parameters = ();
148
149    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
150        use proptest::collection::vec as prop_vec;
151        use smallvec::SmallVec;
152
153        // Generate a small set of simple types for params/results to keep strategies fast/stable
154        let simple_type = prop_oneof![Just(Type::Felt), Just(Type::U32), Just(Type::U64),];
155
156        // Small vectors of params/results
157        let params = prop_vec(simple_type.clone(), 0..=4);
158        let results = prop_vec(simple_type, 0..=2);
159
160        // Use Fast ABI for roundtrip coverage
161        let abi = Just(midenc_hir_type::CallConv::Fast);
162
163        // Option<FunctionType>
164        let signature =
165            prop::option::of((abi, params, results).prop_map(|(abi, params_vec, results_vec)| {
166                let params = SmallVec::<[Type; 4]>::from_vec(params_vec);
167                let results = SmallVec::<[Type; 1]>::from_vec(results_vec);
168                FunctionType { abi, params, results }
169            }));
170
171        let nid = any::<MastNodeId>();
172        let name = any::<QualifiedProcedureName>();
173        (nid, name, signature)
174            .prop_map(|(nodeid, procname, signature)| Self {
175                node: nodeid,
176                path: procname.to_path_buf().into(),
177                signature,
178                attributes: Default::default(),
179            })
180            .boxed()
181    }
182
183    type Strategy = BoxedStrategy<Self>;
184}
185
186#[derive(Debug, Clone, PartialEq, Eq)]
187#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
188#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
189pub struct ConstantExport {
190    /// The fully-qualified path of the exported constant
191    #[cfg_attr(feature = "serde", serde(with = "path"))]
192    pub path: Arc<Path>,
193    /// The constant-folded AST representing the value of this constant
194    pub value: ConstantValue,
195}
196
197#[cfg(feature = "arbitrary")]
198impl Arbitrary for ConstantExport {
199    type Parameters = ();
200
201    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
202        let path = crate::arbitrary::path::constant_path_random_length(1);
203        let value = any::<ConstantValue>();
204
205        (path, value).prop_map(|(path, value)| Self { path, value }).boxed()
206    }
207
208    type Strategy = BoxedStrategy<Self>;
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
212#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
213#[cfg_attr(all(feature = "arbitrary", test), miden_test_serde_macros::serde_test)]
214pub struct TypeExport {
215    /// The fully-qualified path of the exported type declaration
216    #[cfg_attr(feature = "serde", serde(with = "path"))]
217    pub path: Arc<Path>,
218    /// The type bound to `name`
219    pub ty: Type,
220}
221
222#[cfg(feature = "arbitrary")]
223impl Arbitrary for TypeExport {
224    type Parameters = ();
225
226    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
227        use proptest::strategy::{Just, Strategy};
228        let path = crate::arbitrary::path::user_defined_type_path_random_length(1);
229        let ty = Just(Type::Felt);
230
231        (path, ty).prop_map(|(path, ty)| Self { path, ty }).boxed()
232    }
233
234    type Strategy = BoxedStrategy<Self>;
235}
236
237// LIBRARY
238// ================================================================================================
239
240/// Represents a library where all modules were compiled into a [`MastForest`].
241///
242/// A library exports a set of one or more procedures. Currently, all exported procedures belong
243/// to the same top-level namespace.
244#[derive(Debug, Clone, PartialEq, Eq)]
245#[cfg_attr(
246    all(feature = "arbitrary", test),
247    miden_test_serde_macros::serde_test(binary_serde(true))
248)]
249pub struct Library {
250    /// The content hash of this library, formed by hashing the roots of all exports in
251    /// lexicographical order (by digest, not procedure name)
252    digest: Word,
253    /// A map between procedure paths and the corresponding procedure metadata in the MAST forest.
254    /// Multiple paths can map to the same root, and also, some roots may not be associated with
255    /// any paths.
256    ///
257    /// Note that we use `MastNodeId` as an identifier for procedures instead of MAST root, since 2
258    /// different procedures with the same MAST root can be different due to the decorators they
259    /// contain. However, note that `MastNodeId` is also not a unique identifier for procedures; if
260    /// the procedures have the same MAST root and decorators, they will have the same
261    /// `MastNodeId`.
262    exports: BTreeMap<Arc<Path>, LibraryExport>,
263    /// The MAST forest underlying this library.
264    mast_forest: Arc<MastForest>,
265}
266
267impl AsRef<Library> for Library {
268    #[inline(always)]
269    fn as_ref(&self) -> &Library {
270        self
271    }
272}
273
274// ------------------------------------------------------------------------------------------------
275/// Constructors
276impl Library {
277    /// Constructs a new [`Library`] from the provided MAST forest and a set of exports.
278    ///
279    /// # Errors
280    /// Returns an error if the set of exports is empty.
281    /// Returns an error if any of the specified exports do not have a corresponding procedure root
282    /// in the provided MAST forest.
283    pub fn new(
284        mast_forest: Arc<MastForest>,
285        exports: BTreeMap<Arc<Path>, LibraryExport>,
286    ) -> Result<Self, LibraryError> {
287        if exports.is_empty() {
288            return Err(LibraryError::NoExport);
289        }
290
291        for export in exports.values() {
292            if let LibraryExport::Procedure(ProcedureExport { node, path, .. }) = export
293                && !mast_forest.is_procedure_root(*node)
294            {
295                return Err(LibraryError::NoProcedureRootForExport {
296                    procedure_path: path.clone(),
297                });
298            }
299        }
300
301        let digest =
302            mast_forest.compute_nodes_commitment(exports.values().filter_map(
303                |export| match export {
304                    LibraryExport::Procedure(export) => Some(&export.node),
305                    LibraryExport::Constant(_) | LibraryExport::Type(_) => None,
306                },
307            ));
308
309        Ok(Self { digest, exports, mast_forest })
310    }
311
312    /// Produces a new library with the existing [`MastForest`] and where all key/values in the
313    /// provided advice map are added to the internal advice map.
314    pub fn with_advice_map(mut self, advice_map: AdviceMap) -> Self {
315        self.extend_advice_map(advice_map);
316        self
317    }
318
319    /// Extends the advice map of this library
320    pub fn extend_advice_map(&mut self, advice_map: AdviceMap) {
321        let mast_forest = Arc::make_mut(&mut self.mast_forest);
322        mast_forest.advice_map_mut().extend(advice_map);
323    }
324
325    fn read_mast_forest<R: ByteReader>(
326        source: &mut R,
327        validate_mast_forest: bool,
328    ) -> Result<Arc<MastForest>, DeserializationError> {
329        let mast_forest = if validate_mast_forest {
330            UntrustedMastForest::read_from(source)?.validate().map_err(|err| {
331                DeserializationError::InvalidValue(format!(
332                    "library contains an invalid untrusted MAST forest: {err}"
333                ))
334            })?
335        } else {
336            MastForest::read_from(source)?
337        };
338
339        Ok(Arc::new(mast_forest))
340    }
341
342    fn read_from_with_mast_forest<R: ByteReader>(
343        source: &mut R,
344        mast_forest: Arc<MastForest>,
345    ) -> Result<Self, DeserializationError> {
346        let num_exports = source.read_usize()?;
347        if num_exports == 0 {
348            return Err(DeserializationError::InvalidValue(String::from("No exported procedures")));
349        };
350        let mut exports = BTreeMap::new();
351        for _ in 0..num_exports {
352            let tag = source.read_u8()?;
353            let path: PathBuf = source.read()?;
354            let path = Arc::<Path>::from(path.into_boxed_path());
355            let export = match tag {
356                0 => {
357                    let node = MastNodeId::from_u32_safe(source.read_u32()?, &mast_forest)?;
358                    let signature = if source.read_bool()? {
359                        Some(FunctionType::read_from(source)?)
360                    } else {
361                        None
362                    };
363                    let attributes = AttributeSet::read_from(source)?;
364                    LibraryExport::Procedure(ProcedureExport {
365                        node,
366                        path: path.clone(),
367                        signature,
368                        attributes,
369                    })
370                },
371                1 => {
372                    let value = ConstantValue::read_from(source)?;
373                    LibraryExport::Constant(ConstantExport { path: path.clone(), value })
374                },
375                2 => {
376                    let ty = Type::read_from(source)?;
377                    LibraryExport::Type(TypeExport { path: path.clone(), ty })
378                },
379                invalid => {
380                    return Err(DeserializationError::InvalidValue(format!(
381                        "unknown LibraryExport tag: '{invalid}'"
382                    )));
383                },
384            };
385            let (path, export) = normalize_export_for_deserialization(export)
386                .map_err(DeserializationError::InvalidValue)?;
387            if exports.insert(path.clone(), export).is_some() {
388                return Err(DeserializationError::InvalidValue(format!(
389                    "duplicate canonical export path in library artifact: '{path}'"
390                )));
391            }
392        }
393
394        Self::new(mast_forest, exports)
395            .map_err(|err| DeserializationError::InvalidValue(format!("{err}")))
396    }
397
398    /// Reads a library from `source` without validating the embedded MAST forest.
399    ///
400    /// This is only correct when serialization and deserialization happen within the same trust
401    /// domain. A typical use case is reloading bytes that were already validated before being
402    /// persisted to local storage controlled by the same trusted system.
403    ///
404    /// Do not use this for inbound artifact processing across a trust boundary, including bytes
405    /// received over the network or from another party. Authenticating the outer byte stream does
406    /// not prove that embedded MAST node digests are semantically valid.
407    pub fn read_from_unchecked<R: ByteReader>(
408        source: &mut R,
409    ) -> Result<Self, DeserializationError> {
410        let mast_forest = Self::read_mast_forest(source, false)?;
411        Self::read_from_with_mast_forest(source, mast_forest)
412    }
413
414    /// Reads a library from `bytes` without validating the embedded MAST forest.
415    ///
416    /// See [`Library::read_from_unchecked`].
417    pub fn read_from_bytes_unchecked(bytes: &[u8]) -> Result<Self, DeserializationError> {
418        let mut source = SliceReader::new(bytes);
419        Self::read_from_unchecked(&mut source)
420    }
421}
422
423// ------------------------------------------------------------------------------------------------
424/// Public accessors
425impl Library {
426    /// Returns the [Word] representing the content hash of this library
427    pub fn digest(&self) -> &Word {
428        &self.digest
429    }
430
431    /// Returns the fully qualified name and metadata of all procedures exported by the library.
432    pub fn exports(&self) -> impl Iterator<Item = &LibraryExport> {
433        self.exports.values()
434    }
435
436    /// Returns the number of exports in this library.
437    pub fn num_exports(&self) -> usize {
438        self.exports.len()
439    }
440
441    /// Returns a MAST node ID associated with the specified exported procedure.
442    ///
443    /// # Panics
444    /// Panics if the specified procedure is not exported from this library.
445    pub fn get_export_node_id(&self, path: impl AsRef<Path>) -> MastNodeId {
446        let path = path.as_ref().to_absolute();
447        self.exports
448            .get(path.as_ref())
449            .expect("procedure not exported from the library")
450            .unwrap_procedure()
451            .node
452    }
453
454    /// Returns true if the specified exported procedure is re-exported from a dependency.
455    pub fn is_reexport(&self, path: impl AsRef<Path>) -> bool {
456        let path = path.as_ref().to_absolute();
457        self.exports
458            .get(path.as_ref())
459            .and_then(LibraryExport::as_procedure)
460            .map(|export| self.mast_forest[export.node].is_external())
461            .unwrap_or(false)
462    }
463
464    /// Returns a reference to the inner [`MastForest`].
465    pub fn mast_forest(&self) -> &Arc<MastForest> {
466        &self.mast_forest
467    }
468
469    /// Returns the digest of the procedure with the specified name, or `None` if it was not found
470    /// in the library or its library path is malformed.
471    pub fn get_procedure_root_by_path(&self, path: impl AsRef<Path>) -> Option<Word> {
472        let path = path.as_ref().to_absolute();
473        let export = self.exports.get(path.as_ref()).and_then(LibraryExport::as_procedure);
474        export.map(|e| self.mast_forest()[e.node].digest())
475    }
476
477    /// Returns the exact procedure node for the specified path, if it is present.
478    pub fn get_procedure_node_by_path(&self, path: impl AsRef<Path>) -> Option<MastNodeId> {
479        let path = path.as_ref().to_absolute();
480        self.exports
481            .get(path.as_ref())
482            .and_then(LibraryExport::as_procedure)
483            .map(|export| export.node)
484    }
485}
486
487/// Conversions
488impl Library {
489    /// Returns an iterator over the module infos of the library.
490    pub fn module_infos(&self) -> impl Iterator<Item = ModuleInfo> {
491        let mut modules_by_path: BTreeMap<Arc<Path>, ModuleInfo> = BTreeMap::new();
492
493        for export in self.exports.values() {
494            let module_name =
495                Arc::from(export.path().parent().unwrap().to_path_buf().into_boxed_path());
496            let module = modules_by_path
497                .entry(Arc::clone(&module_name))
498                .or_insert_with(|| ModuleInfo::new(module_name, None));
499            match export {
500                LibraryExport::Procedure(ProcedureExport { node, path, signature, attributes }) => {
501                    let proc_digest = self.mast_forest[*node].digest();
502                    let name = path.last().unwrap();
503                    module.add_procedure_with_provenance(
504                        ProcedureName::new(name).expect("valid procedure name"),
505                        proc_digest,
506                        signature.clone().map(Arc::new),
507                        attributes.clone(),
508                        Some(*node),
509                        Some(self.mast_forest.commitment()),
510                    );
511                },
512                LibraryExport::Constant(ConstantExport { path, value }) => {
513                    let name = Ident::new(path.last().unwrap()).expect("valid identifier");
514                    module.add_constant(name, value.clone());
515                },
516                LibraryExport::Type(TypeExport { path, ty }) => {
517                    let name = Ident::new(path.last().unwrap()).expect("valid identifier");
518                    module.add_type(name, ty.clone());
519                },
520            }
521        }
522
523        modules_by_path.into_values()
524    }
525}
526
527#[cfg(feature = "std")]
528impl Library {
529    /// File extension for the Assembly Library.
530    pub const LIBRARY_EXTENSION: &'static str = "masl";
531
532    /// Write the library to a target file
533    ///
534    /// NOTE: It is up to the caller to use the correct file extension, but there is no
535    /// specific requirement that the extension be set, or the same as
536    /// [`Self::LIBRARY_EXTENSION`].
537    pub fn write_to_file(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
538        let path = path.as_ref();
539
540        if let Some(dir) = path.parent() {
541            std::fs::create_dir_all(dir)?;
542        }
543
544        // NOTE: We catch panics due to i/o errors here due to the fact that the ByteWriter
545        // trait does not provide fallible APIs, so WriteAdapter will panic if the underlying
546        // writes fail. This needs to be addressed upstream at some point
547        std::panic::catch_unwind(|| {
548            let mut file = std::fs::File::create(path)?;
549            self.write_into(&mut file);
550            Ok(())
551        })
552        .map_err(|p| match p.downcast::<std::io::Error>() {
553            Ok(err) => *err,
554            Err(err) => std::panic::resume_unwind(err),
555        })?
556    }
557
558    pub fn deserialize_from_file(
559        path: impl AsRef<std::path::Path>,
560    ) -> Result<Self, DeserializationError> {
561        use miden_core::utils::ReadAdapter;
562
563        let path = path.as_ref();
564        let mut file = std::fs::File::open(path).map_err(|err| {
565            DeserializationError::InvalidValue(format!(
566                "failed to open file at {}: {err}",
567                path.to_string_lossy()
568            ))
569        })?;
570        let mut adapter = ReadAdapter::new(&mut file);
571
572        Self::read_from(&mut adapter)
573    }
574}
575
576// KERNEL LIBRARY
577// ================================================================================================
578
579/// Represents a library containing a Miden VM kernel.
580///
581/// This differs from the regular [Library] as follows:
582/// - All exported procedures must be exported directly from the kernel namespace (i.e., `$kernel`).
583/// - There must be at least one exported procedure.
584/// - The number of exported procedures cannot exceed [Kernel::MAX_NUM_PROCEDURES] (i.e., 256).
585#[derive(Debug, Clone, PartialEq, Eq)]
586#[cfg_attr(feature = "serde", derive(Deserialize))]
587#[cfg_attr(feature = "serde", serde(try_from = "Arc<Library>"))]
588pub struct KernelLibrary {
589    #[cfg_attr(feature = "serde", serde(skip))]
590    kernel: Kernel,
591    #[cfg_attr(feature = "serde", serde(skip))]
592    kernel_info: ModuleInfo,
593    library: Arc<Library>,
594}
595
596#[cfg(feature = "serde")]
597impl Serialize for KernelLibrary {
598    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
599    where
600        S: serde::Serializer,
601    {
602        Library::serialize(&self.library, serializer)
603    }
604}
605
606impl AsRef<Library> for KernelLibrary {
607    #[inline(always)]
608    fn as_ref(&self) -> &Library {
609        &self.library
610    }
611}
612
613impl KernelLibrary {
614    fn try_from_library(library: Library) -> Result<Self, DeserializationError> {
615        Self::try_from(Arc::new(library)).map_err(|err| {
616            DeserializationError::InvalidValue(format!(
617                "Failed to deserialize kernel library: {err}"
618            ))
619        })
620    }
621
622    /// Returns the [Kernel] for this kernel library.
623    pub fn kernel(&self) -> &Kernel {
624        &self.kernel
625    }
626
627    /// Returns a reference to the inner [`MastForest`].
628    pub fn mast_forest(&self) -> &Arc<MastForest> {
629        self.library.mast_forest()
630    }
631
632    /// Destructures this kernel library into individual parts.
633    pub fn into_parts(self) -> (Kernel, ModuleInfo, Arc<MastForest>) {
634        (self.kernel, self.kernel_info, self.library.mast_forest().clone())
635    }
636
637    /// Reads a kernel library from `source` without validating the embedded MAST forest.
638    ///
639    /// This is only correct when serialization and deserialization happen within the same trust
640    /// domain. A typical use case is reloading bytes that were already validated before being
641    /// persisted to local storage controlled by the same trusted system.
642    ///
643    /// Do not use this for inbound artifact processing across a trust boundary, including bytes
644    /// received over the network or from another party. Authenticating the outer byte stream does
645    /// not prove that embedded MAST node digests are semantically valid.
646    pub fn read_from_unchecked<R: ByteReader>(
647        source: &mut R,
648    ) -> Result<Self, DeserializationError> {
649        let library = Library::read_from_unchecked(source)?;
650        Self::try_from_library(library)
651    }
652
653    /// Reads a kernel library from `bytes` without validating the embedded MAST forest.
654    ///
655    /// See [`KernelLibrary::read_from_unchecked`].
656    pub fn read_from_bytes_unchecked(bytes: &[u8]) -> Result<Self, DeserializationError> {
657        let mut source = SliceReader::new(bytes);
658        Self::read_from_unchecked(&mut source)
659    }
660}
661
662impl TryFrom<Arc<Library>> for KernelLibrary {
663    type Error = LibraryError;
664
665    fn try_from(library: Arc<Library>) -> Result<Self, Self::Error> {
666        let kernel_path = Arc::from(Path::kernel_path().to_path_buf().into_boxed_path());
667        let mut proc_digests = Vec::with_capacity(library.exports.len());
668
669        let mut kernel_module = ModuleInfo::new(Arc::clone(&kernel_path), None);
670
671        for export in library.exports.values() {
672            match export {
673                LibraryExport::Procedure(export) => {
674                    // make sure all procedures are exported only from the kernel root
675                    if !export.path.is_in_kernel() {
676                        return Err(LibraryError::InvalidKernelExport {
677                            procedure_path: export.path.clone(),
678                        });
679                    }
680
681                    let proc_digest = library.mast_forest[export.node].digest();
682                    proc_digests.push(proc_digest);
683                    kernel_module.add_procedure_with_provenance(
684                        ProcedureName::new(export.path.last().unwrap())
685                            .expect("valid procedure name"),
686                        proc_digest,
687                        export.signature.clone().map(Arc::new),
688                        export.attributes.clone(),
689                        Some(export.node),
690                        Some(library.mast_forest.commitment()),
691                    );
692                },
693                LibraryExport::Constant(export) => {
694                    // Only export constants from the kernel root
695                    if export.path.is_in_kernel() {
696                        let name =
697                            Ident::new(export.path.last().unwrap()).expect("valid identifier");
698                        kernel_module.add_constant(name, export.value.clone());
699                    }
700                },
701                LibraryExport::Type(export) => {
702                    // Only export types from the kernel root
703                    if export.path.is_in_kernel() {
704                        let name =
705                            Ident::new(export.path.last().unwrap()).expect("valid identifier");
706                        kernel_module.add_type(name, export.ty.clone());
707                    }
708                },
709            }
710        }
711
712        if proc_digests.is_empty() {
713            return Err(LibraryError::NoExport);
714        }
715
716        let kernel = Kernel::new(&proc_digests).map_err(LibraryError::KernelConversion)?;
717
718        Ok(Self {
719            kernel,
720            kernel_info: kernel_module,
721            library,
722        })
723    }
724}
725
726#[cfg(feature = "std")]
727impl KernelLibrary {
728    /// Write the library to a target file
729    pub fn write_to_file(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
730        self.library.write_to_file(path)
731    }
732}
733
734// LIBRARY SERIALIZATION
735// ================================================================================================
736
737fn export_raw_leaf(path: &Path) -> Result<&str, String> {
738    match path.components().next_back() {
739        Some(Ok(PathComponent::Normal(leaf))) => Ok(leaf),
740        Some(Err(err)) => Err(format!("invalid export path '{path}': {err}")),
741        Some(Ok(PathComponent::Root)) | None => {
742            Err(format!("invalid export path (missing export leaf): '{path}'"))
743        },
744    }
745}
746
747fn canonicalize_export_path(path: &Path) -> Result<Arc<Path>, String> {
748    let canonical = path
749        .to_path_buf()
750        .canonicalize()
751        .map_err(|err| format!("invalid export path '{path}': {err}"))?;
752    Ok(Arc::<Path>::from(canonical.into_boxed_path()))
753}
754
755fn normalize_export_for_deserialization(
756    mut export: LibraryExport,
757) -> Result<(Arc<Path>, LibraryExport), String> {
758    let canonical_path = canonicalize_export_path(export.path().as_ref())?;
759    let leaf = export_raw_leaf(canonical_path.as_ref())?;
760
761    match &export {
762        LibraryExport::Procedure(_) => {
763            ProcedureName::new(leaf).map_err(|err| {
764                format!(
765                    "invalid procedure export leaf name '{leaf}' in path '{canonical_path}': {err}"
766                )
767            })?;
768        },
769        LibraryExport::Constant(_) | LibraryExport::Type(_) => {
770            Ident::new(leaf).map_err(|err| {
771                format!("invalid export leaf name '{leaf}' in path '{canonical_path}': {err}")
772            })?;
773        },
774    }
775
776    match &mut export {
777        LibraryExport::Procedure(export) => export.path = canonical_path.clone(),
778        LibraryExport::Constant(export) => export.path = canonical_path.clone(),
779        LibraryExport::Type(export) => export.path = canonical_path.clone(),
780    }
781
782    Ok((canonical_path, export))
783}
784
785/// NOTE: Serialization of libraries is likely to be deprecated in a future release
786impl Serializable for Library {
787    fn write_into<W: ByteWriter>(&self, target: &mut W) {
788        let Self { digest: _, exports, mast_forest } = self;
789
790        mast_forest.write_into(target);
791
792        target.write_usize(exports.len());
793        for export in exports.values() {
794            export.write_into(target);
795        }
796    }
797}
798
799/// NOTE: Serialization of libraries is likely to be deprecated in a future release
800impl Deserializable for Library {
801    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
802        let mast_forest = Self::read_mast_forest(source, true)?;
803        Self::read_from_with_mast_forest(source, mast_forest)
804    }
805}
806
807#[cfg(feature = "serde")]
808impl Serialize for Library {
809    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
810    where
811        S: serde::Serializer,
812    {
813        use serde::ser::SerializeStruct;
814
815        struct LibraryExports<'a>(&'a BTreeMap<Arc<Path>, LibraryExport>);
816        impl Serialize for LibraryExports<'_> {
817            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
818            where
819                S: serde::Serializer,
820            {
821                use serde::ser::SerializeSeq;
822                let mut serializer = serializer.serialize_seq(Some(self.0.len()))?;
823                for elem in self.0.values() {
824                    serializer.serialize_element(elem)?;
825                }
826                serializer.end()
827            }
828        }
829
830        let Self { digest: _, exports, mast_forest } = self;
831
832        let mut serializer = serializer.serialize_struct("Library", 2)?;
833        serializer.serialize_field("mast_forest", mast_forest)?;
834        serializer.serialize_field("exports", &LibraryExports(exports))?;
835        serializer.end()
836    }
837}
838
839#[cfg(feature = "serde")]
840impl<'de> Deserialize<'de> for Library {
841    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
842    where
843        D: serde::Deserializer<'de>,
844    {
845        use serde::de::Visitor;
846
847        #[derive(Deserialize)]
848        #[serde(field_identifier, rename_all = "snake_case")]
849        enum Field {
850            MastForest,
851            Exports,
852        }
853
854        struct LibraryVisitor;
855
856        impl LibraryVisitor {
857            fn normalize_exports<E>(
858                items: Vec<LibraryExport>,
859            ) -> Result<BTreeMap<Arc<Path>, LibraryExport>, E>
860            where
861                E: serde::de::Error,
862            {
863                let mut exports = BTreeMap::new();
864                for export in items {
865                    let (path, export) = normalize_export_for_deserialization(export)
866                        .map_err(serde::de::Error::custom)?;
867                    if exports.insert(path.clone(), export).is_some() {
868                        return Err(serde::de::Error::custom(format!(
869                            "duplicate canonical export path in library artifact: '{path}'"
870                        )));
871                    }
872                }
873
874                Ok(exports)
875            }
876        }
877
878        impl<'de> Visitor<'de> for LibraryVisitor {
879            type Value = Library;
880
881            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
882                formatter.write_str("struct Library")
883            }
884
885            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
886            where
887                A: serde::de::SeqAccess<'de>,
888            {
889                let mast_forest = seq
890                    .next_element()?
891                    .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
892                let export_items: Vec<LibraryExport> = seq
893                    .next_element()?
894                    .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
895                let exports = Self::normalize_exports(export_items)?;
896                Library::new(mast_forest, exports).map_err(serde::de::Error::custom)
897            }
898
899            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
900            where
901                A: serde::de::MapAccess<'de>,
902            {
903                let mut mast_forest = None;
904                let mut exports = None;
905                while let Some(key) = map.next_key()? {
906                    match key {
907                        Field::MastForest => {
908                            if mast_forest.is_some() {
909                                return Err(serde::de::Error::duplicate_field("mast_forest"));
910                            }
911                            mast_forest = Some(map.next_value()?);
912                        },
913                        Field::Exports => {
914                            if exports.is_some() {
915                                return Err(serde::de::Error::duplicate_field("exports"));
916                            }
917                            let items: Vec<LibraryExport> = map.next_value()?;
918                            exports = Some(Self::normalize_exports(items)?);
919                        },
920                    }
921                }
922                let mast_forest =
923                    mast_forest.ok_or_else(|| serde::de::Error::missing_field("mast_forest"))?;
924                let exports = exports.ok_or_else(|| serde::de::Error::missing_field("exports"))?;
925                Library::new(mast_forest, exports).map_err(serde::de::Error::custom)
926            }
927        }
928
929        deserializer.deserialize_struct("Library", &["mast_forest", "exports"], LibraryVisitor)
930    }
931}
932
933/// NOTE: Serialization of libraries is likely to be deprecated in a future release
934impl Serializable for KernelLibrary {
935    fn write_into<W: ByteWriter>(&self, target: &mut W) {
936        let Self { kernel: _, kernel_info: _, library } = self;
937
938        library.write_into(target);
939    }
940}
941
942/// NOTE: Serialization of libraries is likely to be deprecated in a future release
943impl Deserializable for KernelLibrary {
944    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
945        let library = Library::read_from(source)?;
946        Self::try_from_library(library)
947    }
948}
949
950/// NOTE: Deserialization is handled in the implementation for [Library]
951impl Serializable for LibraryExport {
952    fn write_into<W: ByteWriter>(&self, target: &mut W) {
953        match self {
954            LibraryExport::Procedure(ProcedureExport {
955                node,
956                path: name,
957                signature,
958                attributes,
959            }) => {
960                target.write_u8(0);
961                name.write_into(target);
962                target.write_u32(u32::from(*node));
963                if let Some(sig) = signature {
964                    target.write_bool(true);
965                    sig.write_into(target);
966                } else {
967                    target.write_bool(false);
968                }
969                attributes.write_into(target);
970            },
971            LibraryExport::Constant(ConstantExport { path: name, value }) => {
972                target.write_u8(1);
973                name.write_into(target);
974                value.write_into(target);
975            },
976            LibraryExport::Type(TypeExport { path: name, ty }) => {
977                target.write_u8(2);
978                name.write_into(target);
979                ty.write_into(target);
980            },
981        }
982    }
983}
984
985#[cfg(feature = "arbitrary")]
986impl Arbitrary for Library {
987    type Parameters = ();
988
989    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
990        use miden_core::{
991            mast::{BasicBlockNodeBuilder, MastForestContributor},
992            operations::Operation,
993        };
994        use proptest::prelude::*;
995
996        prop::collection::vec(any::<LibraryExport>(), 1..5)
997            .prop_map(|exports| {
998                let mut exports =
999                    BTreeMap::from_iter(exports.into_iter().map(|export| (export.path(), export)));
1000                // Create a MastForest with actual nodes for the exports
1001                let mut mast_forest = MastForest::new();
1002                let mut nodes = Vec::new();
1003
1004                for export in exports.values() {
1005                    if let LibraryExport::Procedure(export) = export {
1006                        let node_id = BasicBlockNodeBuilder::new(
1007                            vec![Operation::Add, Operation::Mul],
1008                            Vec::new(),
1009                        )
1010                        .add_to_forest(&mut mast_forest)
1011                        .unwrap();
1012                        nodes.push((export.node, node_id));
1013                    }
1014                }
1015
1016                // Replace the export node IDs with the actual node IDs we created
1017                let mut procedure_exports = 0;
1018                for export in exports.values_mut() {
1019                    match export {
1020                        LibraryExport::Procedure(export) => {
1021                            procedure_exports += 1;
1022                            // Find the corresponding node we created
1023                            if let Some(&(_, actual_node_id)) =
1024                                nodes.iter().find(|(original_id, _)| *original_id == export.node)
1025                            {
1026                                export.node = actual_node_id;
1027                            } else {
1028                                // If we can't find the node (shouldn't happen), use the first node
1029                                // we created
1030                                if let Some(&(_, first_node_id)) = nodes.first() {
1031                                    export.node = first_node_id;
1032                                } else {
1033                                    // This should never happen since we create nodes for each
1034                                    // export
1035                                    panic!("No nodes created for exports");
1036                                }
1037                            }
1038                        },
1039                        LibraryExport::Constant(_) | LibraryExport::Type(_) => (),
1040                    }
1041                }
1042
1043                let mut node_ids = Vec::with_capacity(procedure_exports);
1044                for export in exports.values() {
1045                    if let LibraryExport::Procedure(export) = export {
1046                        // Add the node to the forest roots if it's not already there
1047                        mast_forest.make_root(export.node);
1048                        // Collect the node id for recomputing the digest
1049                        node_ids.push(export.node);
1050                    }
1051                }
1052
1053                // Recompute the digest
1054                let digest = mast_forest.compute_nodes_commitment(&node_ids);
1055
1056                let mast_forest = Arc::new(mast_forest);
1057                Library { digest, exports, mast_forest }
1058            })
1059            .boxed()
1060    }
1061
1062    type Strategy = BoxedStrategy<Self>;
1063}
1064
1065#[cfg(test)]
1066mod tests {
1067    #[cfg(feature = "serde")]
1068    use super::*;
1069
1070    #[cfg(feature = "serde")]
1071    #[test]
1072    fn serde_library_deserialization_rejects_duplicate_canonical_export_paths() {
1073        let quoted = Arc::<Path>::from(Path::validate(r#"::foo::"bar""#).unwrap());
1074        let unquoted = Arc::<Path>::from(Path::validate("::foo::bar").unwrap());
1075
1076        let mut exports = BTreeMap::new();
1077        exports.insert(
1078            quoted.clone(),
1079            LibraryExport::Type(TypeExport { path: quoted, ty: Type::Felt }),
1080        );
1081        exports.insert(
1082            unquoted.clone(),
1083            LibraryExport::Type(TypeExport { path: unquoted, ty: Type::Felt }),
1084        );
1085
1086        let lib =
1087            Library::new(Arc::new(MastForest::new()), exports).expect("library must validate");
1088        let json = serde_json::to_string(&lib).expect("library serialization must succeed");
1089
1090        let err = serde_json::from_str::<Library>(&json).expect_err(
1091            "expected duplicate canonical export paths to be rejected during serde deserialization",
1092        );
1093        let message = alloc::format!("{err}");
1094        assert!(
1095            message.contains("duplicate canonical export path in library artifact"),
1096            "unexpected error: {err}"
1097        );
1098    }
1099
1100    #[cfg(feature = "serde")]
1101    #[test]
1102    fn serde_library_deserialization_rejects_malformed_quoted_procedure_leaf() {
1103        use miden_core::{
1104            mast::{BasicBlockNodeBuilder, MastForestContributor},
1105            operations::Operation,
1106        };
1107
1108        let mut mast_forest = MastForest::new();
1109        let node = BasicBlockNodeBuilder::new(vec![Operation::Add], Vec::new())
1110            .add_to_forest(&mut mast_forest)
1111            .expect("must create MAST node");
1112        mast_forest.make_root(node);
1113
1114        let bad = Arc::<Path>::from(Path::validate(r#"::foo::"bad name""#).unwrap());
1115        let mut exports = BTreeMap::new();
1116        exports.insert(bad.clone(), LibraryExport::Procedure(ProcedureExport::new(node, bad)));
1117
1118        let lib = Library::new(Arc::new(mast_forest), exports).expect("library must validate");
1119        let json = serde_json::to_string(&lib).expect("library serialization must succeed");
1120
1121        let err = serde_json::from_str::<Library>(&json)
1122            .expect_err("expected malformed procedure export leaf name rejection during serde");
1123        let message = alloc::format!("{err}");
1124        assert!(
1125            message.contains("invalid procedure export leaf name"),
1126            "unexpected error: {err}"
1127        );
1128    }
1129
1130    #[cfg(feature = "serde")]
1131    #[test]
1132    fn serde_library_deserialization_rejects_malformed_quoted_constant_leaf() {
1133        let bad = Arc::<Path>::from(Path::validate(r#"::foo::"bad name""#).unwrap());
1134        let mut exports = BTreeMap::new();
1135        exports.insert(
1136            bad.clone(),
1137            LibraryExport::Constant(ConstantExport {
1138                path: bad,
1139                value: ConstantValue::Int(miden_debug_types::Span::unknown(
1140                    crate::parser::IntValue::from(1u8),
1141                )),
1142            }),
1143        );
1144
1145        let lib =
1146            Library::new(Arc::new(MastForest::new()), exports).expect("library must validate");
1147        let json = serde_json::to_string(&lib).expect("library serialization must succeed");
1148
1149        let err = serde_json::from_str::<Library>(&json)
1150            .expect_err("expected malformed constant export leaf name rejection during serde");
1151        let message = alloc::format!("{err}");
1152        assert!(message.contains("invalid export leaf name"), "unexpected error: {err}");
1153    }
1154
1155    #[cfg(feature = "serde")]
1156    #[test]
1157    fn serde_library_deserialization_rejects_malformed_quoted_type_leaf() {
1158        let bad = Arc::<Path>::from(Path::validate(r#"::foo::"bad name""#).unwrap());
1159        let mut exports = BTreeMap::new();
1160        exports.insert(bad.clone(), LibraryExport::Type(TypeExport { path: bad, ty: Type::Felt }));
1161
1162        let lib =
1163            Library::new(Arc::new(MastForest::new()), exports).expect("library must validate");
1164        let json = serde_json::to_string(&lib).expect("library serialization must succeed");
1165
1166        let err = serde_json::from_str::<Library>(&json)
1167            .expect_err("expected malformed type export leaf name rejection during serde");
1168        let message = alloc::format!("{err}");
1169        assert!(message.contains("invalid export leaf name"), "unexpected error: {err}");
1170    }
1171}