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
478/// Conversions
479impl Library {
480    /// Returns an iterator over the module infos of the library.
481    pub fn module_infos(&self) -> impl Iterator<Item = ModuleInfo> {
482        let mut modules_by_path: BTreeMap<Arc<Path>, ModuleInfo> = BTreeMap::new();
483
484        for export in self.exports.values() {
485            let module_name =
486                Arc::from(export.path().parent().unwrap().to_path_buf().into_boxed_path());
487            let module = modules_by_path
488                .entry(Arc::clone(&module_name))
489                .or_insert_with(|| ModuleInfo::new(module_name, None));
490            match export {
491                LibraryExport::Procedure(ProcedureExport { node, path, signature, attributes }) => {
492                    let proc_digest = self.mast_forest[*node].digest();
493                    let name = path.last().unwrap();
494                    module.add_procedure(
495                        ProcedureName::new(name).expect("valid procedure name"),
496                        proc_digest,
497                        signature.clone().map(Arc::new),
498                        attributes.clone(),
499                    );
500                },
501                LibraryExport::Constant(ConstantExport { path, value }) => {
502                    let name = Ident::new(path.last().unwrap()).expect("valid identifier");
503                    module.add_constant(name, value.clone());
504                },
505                LibraryExport::Type(TypeExport { path, ty }) => {
506                    let name = Ident::new(path.last().unwrap()).expect("valid identifier");
507                    module.add_type(name, ty.clone());
508                },
509            }
510        }
511
512        modules_by_path.into_values()
513    }
514}
515
516#[cfg(feature = "std")]
517impl Library {
518    /// File extension for the Assembly Library.
519    pub const LIBRARY_EXTENSION: &'static str = "masl";
520
521    /// Write the library to a target file
522    ///
523    /// NOTE: It is up to the caller to use the correct file extension, but there is no
524    /// specific requirement that the extension be set, or the same as
525    /// [`Self::LIBRARY_EXTENSION`].
526    pub fn write_to_file(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
527        let path = path.as_ref();
528
529        if let Some(dir) = path.parent() {
530            std::fs::create_dir_all(dir)?;
531        }
532
533        // NOTE: We catch panics due to i/o errors here due to the fact that the ByteWriter
534        // trait does not provide fallible APIs, so WriteAdapter will panic if the underlying
535        // writes fail. This needs to be addressed upstream at some point
536        std::panic::catch_unwind(|| {
537            let mut file = std::fs::File::create(path)?;
538            self.write_into(&mut file);
539            Ok(())
540        })
541        .map_err(|p| match p.downcast::<std::io::Error>() {
542            Ok(err) => *err,
543            Err(err) => std::panic::resume_unwind(err),
544        })?
545    }
546
547    pub fn deserialize_from_file(
548        path: impl AsRef<std::path::Path>,
549    ) -> Result<Self, DeserializationError> {
550        use miden_core::utils::ReadAdapter;
551
552        let path = path.as_ref();
553        let mut file = std::fs::File::open(path).map_err(|err| {
554            DeserializationError::InvalidValue(format!(
555                "failed to open file at {}: {err}",
556                path.to_string_lossy()
557            ))
558        })?;
559        let mut adapter = ReadAdapter::new(&mut file);
560
561        Self::read_from(&mut adapter)
562    }
563}
564
565// KERNEL LIBRARY
566// ================================================================================================
567
568/// Represents a library containing a Miden VM kernel.
569///
570/// This differs from the regular [Library] as follows:
571/// - All exported procedures must be exported directly from the kernel namespace (i.e., `$kernel`).
572/// - There must be at least one exported procedure.
573/// - The number of exported procedures cannot exceed [Kernel::MAX_NUM_PROCEDURES] (i.e., 256).
574#[derive(Debug, Clone, PartialEq, Eq)]
575#[cfg_attr(feature = "serde", derive(Deserialize))]
576#[cfg_attr(feature = "serde", serde(try_from = "Arc<Library>"))]
577pub struct KernelLibrary {
578    #[cfg_attr(feature = "serde", serde(skip))]
579    kernel: Kernel,
580    #[cfg_attr(feature = "serde", serde(skip))]
581    kernel_info: ModuleInfo,
582    library: Arc<Library>,
583}
584
585#[cfg(feature = "serde")]
586impl Serialize for KernelLibrary {
587    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
588    where
589        S: serde::Serializer,
590    {
591        Library::serialize(&self.library, serializer)
592    }
593}
594
595impl AsRef<Library> for KernelLibrary {
596    #[inline(always)]
597    fn as_ref(&self) -> &Library {
598        &self.library
599    }
600}
601
602impl KernelLibrary {
603    fn try_from_library(library: Library) -> Result<Self, DeserializationError> {
604        Self::try_from(Arc::new(library)).map_err(|err| {
605            DeserializationError::InvalidValue(format!(
606                "Failed to deserialize kernel library: {err}"
607            ))
608        })
609    }
610
611    /// Returns the [Kernel] for this kernel library.
612    pub fn kernel(&self) -> &Kernel {
613        &self.kernel
614    }
615
616    /// Returns a reference to the inner [`MastForest`].
617    pub fn mast_forest(&self) -> &Arc<MastForest> {
618        self.library.mast_forest()
619    }
620
621    /// Destructures this kernel library into individual parts.
622    pub fn into_parts(self) -> (Kernel, ModuleInfo, Arc<MastForest>) {
623        (self.kernel, self.kernel_info, self.library.mast_forest().clone())
624    }
625
626    /// Reads a kernel library from `source` without validating the embedded MAST forest.
627    ///
628    /// This is only correct when serialization and deserialization happen within the same trust
629    /// domain. A typical use case is reloading bytes that were already validated before being
630    /// persisted to local storage controlled by the same trusted system.
631    ///
632    /// Do not use this for inbound artifact processing across a trust boundary, including bytes
633    /// received over the network or from another party. Authenticating the outer byte stream does
634    /// not prove that embedded MAST node digests are semantically valid.
635    pub fn read_from_unchecked<R: ByteReader>(
636        source: &mut R,
637    ) -> Result<Self, DeserializationError> {
638        let library = Library::read_from_unchecked(source)?;
639        Self::try_from_library(library)
640    }
641
642    /// Reads a kernel library from `bytes` without validating the embedded MAST forest.
643    ///
644    /// See [`KernelLibrary::read_from_unchecked`].
645    pub fn read_from_bytes_unchecked(bytes: &[u8]) -> Result<Self, DeserializationError> {
646        let mut source = SliceReader::new(bytes);
647        Self::read_from_unchecked(&mut source)
648    }
649}
650
651impl TryFrom<Arc<Library>> for KernelLibrary {
652    type Error = LibraryError;
653
654    fn try_from(library: Arc<Library>) -> Result<Self, Self::Error> {
655        let kernel_path = Arc::from(Path::kernel_path().to_path_buf().into_boxed_path());
656        let mut proc_digests = Vec::with_capacity(library.exports.len());
657
658        let mut kernel_module = ModuleInfo::new(Arc::clone(&kernel_path), None);
659
660        for export in library.exports.values() {
661            match export {
662                LibraryExport::Procedure(export) => {
663                    // make sure all procedures are exported only from the kernel root
664                    if !export.path.is_in_kernel() {
665                        return Err(LibraryError::InvalidKernelExport {
666                            procedure_path: export.path.clone(),
667                        });
668                    }
669
670                    let proc_digest = library.mast_forest[export.node].digest();
671                    proc_digests.push(proc_digest);
672                    kernel_module.add_procedure(
673                        ProcedureName::new(export.path.last().unwrap())
674                            .expect("valid procedure name"),
675                        proc_digest,
676                        export.signature.clone().map(Arc::new),
677                        export.attributes.clone(),
678                    );
679                },
680                LibraryExport::Constant(export) => {
681                    // Only export constants from the kernel root
682                    if export.path.is_in_kernel() {
683                        let name =
684                            Ident::new(export.path.last().unwrap()).expect("valid identifier");
685                        kernel_module.add_constant(name, export.value.clone());
686                    }
687                },
688                LibraryExport::Type(export) => {
689                    // Only export types from the kernel root
690                    if export.path.is_in_kernel() {
691                        let name =
692                            Ident::new(export.path.last().unwrap()).expect("valid identifier");
693                        kernel_module.add_type(name, export.ty.clone());
694                    }
695                },
696            }
697        }
698
699        if proc_digests.is_empty() {
700            return Err(LibraryError::NoExport);
701        }
702
703        let kernel = Kernel::new(&proc_digests).map_err(LibraryError::KernelConversion)?;
704
705        Ok(Self {
706            kernel,
707            kernel_info: kernel_module,
708            library,
709        })
710    }
711}
712
713#[cfg(feature = "std")]
714impl KernelLibrary {
715    /// Write the library to a target file
716    pub fn write_to_file(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
717        self.library.write_to_file(path)
718    }
719}
720
721// LIBRARY SERIALIZATION
722// ================================================================================================
723
724fn export_raw_leaf(path: &Path) -> Result<&str, String> {
725    match path.components().next_back() {
726        Some(Ok(PathComponent::Normal(leaf))) => Ok(leaf),
727        Some(Err(err)) => Err(format!("invalid export path '{path}': {err}")),
728        Some(Ok(PathComponent::Root)) | None => {
729            Err(format!("invalid export path (missing export leaf): '{path}'"))
730        },
731    }
732}
733
734fn canonicalize_export_path(path: &Path) -> Result<Arc<Path>, String> {
735    let canonical = path
736        .to_path_buf()
737        .canonicalize()
738        .map_err(|err| format!("invalid export path '{path}': {err}"))?;
739    Ok(Arc::<Path>::from(canonical.into_boxed_path()))
740}
741
742fn normalize_export_for_deserialization(
743    mut export: LibraryExport,
744) -> Result<(Arc<Path>, LibraryExport), String> {
745    let canonical_path = canonicalize_export_path(export.path().as_ref())?;
746    let leaf = export_raw_leaf(canonical_path.as_ref())?;
747
748    match &export {
749        LibraryExport::Procedure(_) => {
750            ProcedureName::new(leaf).map_err(|err| {
751                format!(
752                    "invalid procedure export leaf name '{leaf}' in path '{canonical_path}': {err}"
753                )
754            })?;
755        },
756        LibraryExport::Constant(_) | LibraryExport::Type(_) => {
757            Ident::new(leaf).map_err(|err| {
758                format!("invalid export leaf name '{leaf}' in path '{canonical_path}': {err}")
759            })?;
760        },
761    }
762
763    match &mut export {
764        LibraryExport::Procedure(export) => export.path = canonical_path.clone(),
765        LibraryExport::Constant(export) => export.path = canonical_path.clone(),
766        LibraryExport::Type(export) => export.path = canonical_path.clone(),
767    }
768
769    Ok((canonical_path, export))
770}
771
772/// NOTE: Serialization of libraries is likely to be deprecated in a future release
773impl Serializable for Library {
774    fn write_into<W: ByteWriter>(&self, target: &mut W) {
775        let Self { digest: _, exports, mast_forest } = self;
776
777        mast_forest.write_into(target);
778
779        target.write_usize(exports.len());
780        for export in exports.values() {
781            export.write_into(target);
782        }
783    }
784}
785
786/// NOTE: Serialization of libraries is likely to be deprecated in a future release
787impl Deserializable for Library {
788    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
789        let mast_forest = Self::read_mast_forest(source, true)?;
790        Self::read_from_with_mast_forest(source, mast_forest)
791    }
792}
793
794#[cfg(feature = "serde")]
795impl Serialize for Library {
796    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
797    where
798        S: serde::Serializer,
799    {
800        use serde::ser::SerializeStruct;
801
802        struct LibraryExports<'a>(&'a BTreeMap<Arc<Path>, LibraryExport>);
803        impl Serialize for LibraryExports<'_> {
804            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
805            where
806                S: serde::Serializer,
807            {
808                use serde::ser::SerializeSeq;
809                let mut serializer = serializer.serialize_seq(Some(self.0.len()))?;
810                for elem in self.0.values() {
811                    serializer.serialize_element(elem)?;
812                }
813                serializer.end()
814            }
815        }
816
817        let Self { digest: _, exports, mast_forest } = self;
818
819        let mut serializer = serializer.serialize_struct("Library", 2)?;
820        serializer.serialize_field("mast_forest", mast_forest)?;
821        serializer.serialize_field("exports", &LibraryExports(exports))?;
822        serializer.end()
823    }
824}
825
826#[cfg(feature = "serde")]
827impl<'de> Deserialize<'de> for Library {
828    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
829    where
830        D: serde::Deserializer<'de>,
831    {
832        use serde::de::Visitor;
833
834        #[derive(Deserialize)]
835        #[serde(field_identifier, rename_all = "snake_case")]
836        enum Field {
837            MastForest,
838            Exports,
839        }
840
841        struct LibraryVisitor;
842
843        impl LibraryVisitor {
844            fn normalize_exports<E>(
845                items: Vec<LibraryExport>,
846            ) -> Result<BTreeMap<Arc<Path>, LibraryExport>, E>
847            where
848                E: serde::de::Error,
849            {
850                let mut exports = BTreeMap::new();
851                for export in items {
852                    let (path, export) = normalize_export_for_deserialization(export)
853                        .map_err(serde::de::Error::custom)?;
854                    if exports.insert(path.clone(), export).is_some() {
855                        return Err(serde::de::Error::custom(format!(
856                            "duplicate canonical export path in library artifact: '{path}'"
857                        )));
858                    }
859                }
860
861                Ok(exports)
862            }
863        }
864
865        impl<'de> Visitor<'de> for LibraryVisitor {
866            type Value = Library;
867
868            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
869                formatter.write_str("struct Library")
870            }
871
872            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
873            where
874                A: serde::de::SeqAccess<'de>,
875            {
876                let mast_forest = seq
877                    .next_element()?
878                    .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?;
879                let export_items: Vec<LibraryExport> = seq
880                    .next_element()?
881                    .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?;
882                let exports = Self::normalize_exports(export_items)?;
883                Library::new(mast_forest, exports).map_err(serde::de::Error::custom)
884            }
885
886            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
887            where
888                A: serde::de::MapAccess<'de>,
889            {
890                let mut mast_forest = None;
891                let mut exports = None;
892                while let Some(key) = map.next_key()? {
893                    match key {
894                        Field::MastForest => {
895                            if mast_forest.is_some() {
896                                return Err(serde::de::Error::duplicate_field("mast_forest"));
897                            }
898                            mast_forest = Some(map.next_value()?);
899                        },
900                        Field::Exports => {
901                            if exports.is_some() {
902                                return Err(serde::de::Error::duplicate_field("exports"));
903                            }
904                            let items: Vec<LibraryExport> = map.next_value()?;
905                            exports = Some(Self::normalize_exports(items)?);
906                        },
907                    }
908                }
909                let mast_forest =
910                    mast_forest.ok_or_else(|| serde::de::Error::missing_field("mast_forest"))?;
911                let exports = exports.ok_or_else(|| serde::de::Error::missing_field("exports"))?;
912                Library::new(mast_forest, exports).map_err(serde::de::Error::custom)
913            }
914        }
915
916        deserializer.deserialize_struct("Library", &["mast_forest", "exports"], LibraryVisitor)
917    }
918}
919
920/// NOTE: Serialization of libraries is likely to be deprecated in a future release
921impl Serializable for KernelLibrary {
922    fn write_into<W: ByteWriter>(&self, target: &mut W) {
923        let Self { kernel: _, kernel_info: _, library } = self;
924
925        library.write_into(target);
926    }
927}
928
929/// NOTE: Serialization of libraries is likely to be deprecated in a future release
930impl Deserializable for KernelLibrary {
931    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
932        let library = Library::read_from(source)?;
933        Self::try_from_library(library)
934    }
935}
936
937/// NOTE: Deserialization is handled in the implementation for [Library]
938impl Serializable for LibraryExport {
939    fn write_into<W: ByteWriter>(&self, target: &mut W) {
940        match self {
941            LibraryExport::Procedure(ProcedureExport {
942                node,
943                path: name,
944                signature,
945                attributes,
946            }) => {
947                target.write_u8(0);
948                name.write_into(target);
949                target.write_u32(u32::from(*node));
950                if let Some(sig) = signature {
951                    target.write_bool(true);
952                    sig.write_into(target);
953                } else {
954                    target.write_bool(false);
955                }
956                attributes.write_into(target);
957            },
958            LibraryExport::Constant(ConstantExport { path: name, value }) => {
959                target.write_u8(1);
960                name.write_into(target);
961                value.write_into(target);
962            },
963            LibraryExport::Type(TypeExport { path: name, ty }) => {
964                target.write_u8(2);
965                name.write_into(target);
966                ty.write_into(target);
967            },
968        }
969    }
970}
971
972#[cfg(feature = "arbitrary")]
973impl Arbitrary for Library {
974    type Parameters = ();
975
976    fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
977        use miden_core::{
978            mast::{BasicBlockNodeBuilder, MastForestContributor},
979            operations::Operation,
980        };
981        use proptest::prelude::*;
982
983        prop::collection::vec(any::<LibraryExport>(), 1..5)
984            .prop_map(|exports| {
985                let mut exports =
986                    BTreeMap::from_iter(exports.into_iter().map(|export| (export.path(), export)));
987                // Create a MastForest with actual nodes for the exports
988                let mut mast_forest = MastForest::new();
989                let mut nodes = Vec::new();
990
991                for export in exports.values() {
992                    if let LibraryExport::Procedure(export) = export {
993                        let node_id = BasicBlockNodeBuilder::new(
994                            vec![Operation::Add, Operation::Mul],
995                            Vec::new(),
996                        )
997                        .add_to_forest(&mut mast_forest)
998                        .unwrap();
999                        nodes.push((export.node, node_id));
1000                    }
1001                }
1002
1003                // Replace the export node IDs with the actual node IDs we created
1004                let mut procedure_exports = 0;
1005                for export in exports.values_mut() {
1006                    match export {
1007                        LibraryExport::Procedure(export) => {
1008                            procedure_exports += 1;
1009                            // Find the corresponding node we created
1010                            if let Some(&(_, actual_node_id)) =
1011                                nodes.iter().find(|(original_id, _)| *original_id == export.node)
1012                            {
1013                                export.node = actual_node_id;
1014                            } else {
1015                                // If we can't find the node (shouldn't happen), use the first node
1016                                // we created
1017                                if let Some(&(_, first_node_id)) = nodes.first() {
1018                                    export.node = first_node_id;
1019                                } else {
1020                                    // This should never happen since we create nodes for each
1021                                    // export
1022                                    panic!("No nodes created for exports");
1023                                }
1024                            }
1025                        },
1026                        LibraryExport::Constant(_) | LibraryExport::Type(_) => (),
1027                    }
1028                }
1029
1030                let mut node_ids = Vec::with_capacity(procedure_exports);
1031                for export in exports.values() {
1032                    if let LibraryExport::Procedure(export) = export {
1033                        // Add the node to the forest roots if it's not already there
1034                        mast_forest.make_root(export.node);
1035                        // Collect the node id for recomputing the digest
1036                        node_ids.push(export.node);
1037                    }
1038                }
1039
1040                // Recompute the digest
1041                let digest = mast_forest.compute_nodes_commitment(&node_ids);
1042
1043                let mast_forest = Arc::new(mast_forest);
1044                Library { digest, exports, mast_forest }
1045            })
1046            .boxed()
1047    }
1048
1049    type Strategy = BoxedStrategy<Self>;
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054    #[cfg(feature = "serde")]
1055    use super::*;
1056
1057    #[cfg(feature = "serde")]
1058    #[test]
1059    fn serde_library_deserialization_rejects_duplicate_canonical_export_paths() {
1060        let quoted = Arc::<Path>::from(Path::validate(r#"::foo::"bar""#).unwrap());
1061        let unquoted = Arc::<Path>::from(Path::validate("::foo::bar").unwrap());
1062
1063        let mut exports = BTreeMap::new();
1064        exports.insert(
1065            quoted.clone(),
1066            LibraryExport::Type(TypeExport { path: quoted, ty: Type::Felt }),
1067        );
1068        exports.insert(
1069            unquoted.clone(),
1070            LibraryExport::Type(TypeExport { path: unquoted, ty: Type::Felt }),
1071        );
1072
1073        let lib =
1074            Library::new(Arc::new(MastForest::new()), exports).expect("library must validate");
1075        let json = serde_json::to_string(&lib).expect("library serialization must succeed");
1076
1077        let err = serde_json::from_str::<Library>(&json).expect_err(
1078            "expected duplicate canonical export paths to be rejected during serde deserialization",
1079        );
1080        let message = alloc::format!("{err}");
1081        assert!(
1082            message.contains("duplicate canonical export path in library artifact"),
1083            "unexpected error: {err}"
1084        );
1085    }
1086
1087    #[cfg(feature = "serde")]
1088    #[test]
1089    fn serde_library_deserialization_rejects_malformed_quoted_procedure_leaf() {
1090        use miden_core::{
1091            mast::{BasicBlockNodeBuilder, MastForestContributor},
1092            operations::Operation,
1093        };
1094
1095        let mut mast_forest = MastForest::new();
1096        let node = BasicBlockNodeBuilder::new(vec![Operation::Add], Vec::new())
1097            .add_to_forest(&mut mast_forest)
1098            .expect("must create MAST node");
1099        mast_forest.make_root(node);
1100
1101        let bad = Arc::<Path>::from(Path::validate(r#"::foo::"bad name""#).unwrap());
1102        let mut exports = BTreeMap::new();
1103        exports.insert(bad.clone(), LibraryExport::Procedure(ProcedureExport::new(node, bad)));
1104
1105        let lib = Library::new(Arc::new(mast_forest), exports).expect("library must validate");
1106        let json = serde_json::to_string(&lib).expect("library serialization must succeed");
1107
1108        let err = serde_json::from_str::<Library>(&json)
1109            .expect_err("expected malformed procedure export leaf name rejection during serde");
1110        let message = alloc::format!("{err}");
1111        assert!(
1112            message.contains("invalid procedure export leaf name"),
1113            "unexpected error: {err}"
1114        );
1115    }
1116
1117    #[cfg(feature = "serde")]
1118    #[test]
1119    fn serde_library_deserialization_rejects_malformed_quoted_constant_leaf() {
1120        let bad = Arc::<Path>::from(Path::validate(r#"::foo::"bad name""#).unwrap());
1121        let mut exports = BTreeMap::new();
1122        exports.insert(
1123            bad.clone(),
1124            LibraryExport::Constant(ConstantExport {
1125                path: bad,
1126                value: ConstantValue::Int(miden_debug_types::Span::unknown(
1127                    crate::parser::IntValue::from(1u8),
1128                )),
1129            }),
1130        );
1131
1132        let lib =
1133            Library::new(Arc::new(MastForest::new()), exports).expect("library must validate");
1134        let json = serde_json::to_string(&lib).expect("library serialization must succeed");
1135
1136        let err = serde_json::from_str::<Library>(&json)
1137            .expect_err("expected malformed constant export leaf name rejection during serde");
1138        let message = alloc::format!("{err}");
1139        assert!(message.contains("invalid export leaf name"), "unexpected error: {err}");
1140    }
1141
1142    #[cfg(feature = "serde")]
1143    #[test]
1144    fn serde_library_deserialization_rejects_malformed_quoted_type_leaf() {
1145        let bad = Arc::<Path>::from(Path::validate(r#"::foo::"bad name""#).unwrap());
1146        let mut exports = BTreeMap::new();
1147        exports.insert(bad.clone(), LibraryExport::Type(TypeExport { path: bad, ty: Type::Felt }));
1148
1149        let lib =
1150            Library::new(Arc::new(MastForest::new()), exports).expect("library must validate");
1151        let json = serde_json::to_string(&lib).expect("library serialization must succeed");
1152
1153        let err = serde_json::from_str::<Library>(&json)
1154            .expect_err("expected malformed type export leaf name rejection during serde");
1155        let message = alloc::format!("{err}");
1156        assert!(message.contains("invalid export leaf name"), "unexpected error: {err}");
1157    }
1158}