Skip to main content

miden_project/ast/
package.rs

1use alloc::collections::BTreeMap;
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6use super::{
7    parsing::{MaybeInherit, SetSourceId, Validate},
8    *,
9};
10use crate::{Map, MetadataSet, RelatedLabel, SemVer, SourceId, Span, Uri};
11
12/// Represents the contents of the `[package]` table
13#[derive(Debug, Clone)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
16pub struct PackageTable {
17    /// The name of this package
18    pub name: Span<Arc<str>>,
19    /// Additional package information, optionally inheritable from a parent workspace (if present)
20    #[cfg_attr(feature = "serde", serde(flatten))]
21    pub detail: PackageDetail,
22}
23
24impl SetSourceId for PackageTable {
25    fn set_source_id(&mut self, source_id: SourceId) {
26        let Self { name, detail } = self;
27        name.set_source_id(source_id);
28        detail.set_source_id(source_id);
29    }
30}
31
32/// Package properties which may be inherited from a parent workspace
33#[derive(Default, Debug, Clone)]
34#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
35#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
36pub struct PackageDetail {
37    /// The semantic version assigned to this package
38    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
39    pub version: Option<Span<MaybeInherit<SemVer>>>,
40    /// An (optional) brief description of this project
41    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
42    pub description: Option<Span<MaybeInherit<Arc<str>>>>,
43    /// Custom metadata which can be used by third-party/downstream tooling
44    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Map::is_empty"))]
45    pub metadata: MetadataSet,
46}
47
48impl SetSourceId for PackageDetail {
49    fn set_source_id(&mut self, source_id: SourceId) {
50        let Self { version, description, metadata } = self;
51        if let Some(version) = version.as_mut() {
52            version.set_source_id(source_id);
53        }
54        if let Some(description) = description.as_mut() {
55            description.set_source_id(source_id);
56        }
57        metadata.set_source_id(source_id);
58    }
59}
60
61/// Package configuration which can be defined at both the workspace and package level
62#[derive(Default, Debug, Clone)]
63#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
64#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
65pub struct PackageConfig {
66    /// The set of dependencies required by this package/workspace
67    #[cfg_attr(
68        feature = "serde",
69        serde(
70            default,
71            deserialize_with = "dependency::deserialize_dependency_map",
72            skip_serializing_if = "Map::is_empty"
73        )
74    )]
75    pub dependencies: Map<Span<Arc<str>>, Span<DependencySpec>>,
76    /// Linter configuration/overrides for this package/workspace
77    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Map::is_empty"))]
78    pub lints: MetadataSet,
79}
80
81impl SetSourceId for PackageConfig {
82    fn set_source_id(&mut self, source_id: SourceId) {
83        let Self { dependencies, lints } = self;
84        dependencies.set_source_id(source_id);
85        lints.set_source_id(source_id);
86    }
87}
88
89/// Represents the `miden-project.toml` structure of an individual package
90#[derive(Debug, Clone)]
91#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
92#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
93pub struct ProjectFile {
94    /// The original source file this was parsed from, if applicable/known
95    #[cfg_attr(feature = "serde", serde(skip, default))]
96    pub source_file: Option<Arc<SourceFile>>,
97    /// Contents of the `[package]` table
98    pub package: PackageTable,
99    /// Contents of tables shared with workspace-level `miden-project.toml`, e.g. `[dependencies]`
100    /// and `[lints]`
101    #[cfg_attr(feature = "serde", serde(flatten))]
102    pub config: PackageConfig,
103    /// The library target of this project, if applicable
104    #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
105    pub lib: Option<Span<LibTarget>>,
106    /// The binary targets of this project, if applicable
107    #[cfg_attr(
108        feature = "serde",
109        serde(default, rename = "bin", skip_serializing_if = "Vec::is_empty")
110    )]
111    pub bins: Vec<Span<BinTarget>>,
112    /// The set of build profiles defined in this file
113    #[cfg_attr(
114        feature = "serde",
115        serde(
116            default,
117            rename = "profile",
118            deserialize_with = "super::profile::deserialize_profiles_table",
119            skip_serializing_if = "Vec::is_empty"
120        )
121    )]
122    pub profiles: Vec<Profile>,
123}
124
125/// Parsing
126#[cfg(feature = "serde")]
127impl ProjectFile {
128    /// Parse a [ProjectFile] from the provided TOML source file, generally `miden-project.toml`
129    ///
130    /// If successful, the contents of the manifest are semantically valid, with the following
131    /// caveats:
132    ///
133    /// * Inherited properties from the workspace-level are assumed to exist and be correct. It is
134    ///   up to the caller to compute the concrete property values and validate them at that point.
135    pub fn parse(source: Arc<SourceFile>) -> Result<Self, Report> {
136        use parsing::{SetSourceId, Validate};
137
138        let source_id = source.id();
139
140        // Parse the unvalidated project from source
141        let mut package = toml::from_str::<Self>(source.as_str()).map_err(|err| {
142            let span = err
143                .span()
144                .map(|span| {
145                    let start = span.start as u32;
146                    let end = span.end as u32;
147                    SourceSpan::new(source_id, start..end)
148                })
149                .unwrap_or_default();
150            Report::from(ProjectFileError::ParseError {
151                message: err.message().to_string(),
152                source_file: source.clone(),
153                span,
154            })
155        })?;
156
157        package.source_file = Some(source.clone());
158        package.set_source_id(source_id);
159
160        package.validate(source)?;
161
162        Ok(package)
163    }
164
165    pub fn get_or_inherit_version(
166        &self,
167        source: Arc<SourceFile>,
168        workspace: Option<&WorkspaceFile>,
169    ) -> Result<Span<SemVer>, Report> {
170        use core::num::NonZeroU32;
171
172        let Some(version) = self.package.detail.version.as_ref() else {
173            let one = NonZeroU32::new(1).unwrap();
174            let span = source
175                .line_column_to_span(one.into(), one.into())
176                .unwrap_or(source.source_span());
177            return Err(ProjectFileError::MissingVersion { source_file: source, span }.into());
178        };
179        match version.inner() {
180            MaybeInherit::Value(value) => Ok(Span::new(version.span(), value.clone())),
181            MaybeInherit::Inherit => match workspace {
182                Some(workspace) => {
183                    if let Some(version) = workspace.workspace.package.version.as_ref() {
184                        Ok(version.as_ref().map(|inherit| inherit.unwrap_value().clone()))
185                    } else {
186                        Err(ProjectFileError::MissingWorkspaceVersion {
187                            source_file: source,
188                            span: version.span(),
189                        }
190                        .into())
191                    }
192                },
193                None => Err(ProjectFileError::NotAWorkspace {
194                    source_file: source,
195                    span: version.span(),
196                }
197                .into()),
198            },
199        }
200    }
201
202    pub fn get_or_inherit_description(
203        &self,
204        source: Arc<SourceFile>,
205        workspace: Option<&WorkspaceFile>,
206    ) -> Result<Option<Arc<str>>, Report> {
207        match self.package.detail.description.as_ref() {
208            None => Ok(None),
209            Some(desc) => match desc.inner() {
210                MaybeInherit::Value(value) => Ok(Some(value.clone())),
211                MaybeInherit::Inherit => match workspace {
212                    Some(workspace) => Ok(workspace
213                        .workspace
214                        .package
215                        .description
216                        .as_ref()
217                        .map(|d| d.inner().unwrap_value().clone())),
218                    None => Err(ProjectFileError::NotAWorkspace {
219                        source_file: source,
220                        span: desc.span(),
221                    }
222                    .into()),
223                },
224            },
225        }
226    }
227
228    pub fn extract_dependencies(
229        &self,
230        source: Arc<SourceFile>,
231        workspace: Option<&WorkspaceFile>,
232    ) -> Result<Vec<crate::Dependency>, Report> {
233        use crate::{Dependency, DependencyVersionScheme};
234
235        let mut dependencies = Vec::with_capacity(self.config.dependencies.len());
236        for dependency in self.config.dependencies.values() {
237            if dependency.inherits_workspace_version() {
238                if let Some(workspace) = workspace {
239                    match workspace.workspace.config.dependencies.get(&dependency.name) {
240                        Some(dep) => {
241                            debug_assert!(!dep.inherits_workspace_version());
242
243                            let version = DependencyVersionScheme::try_from_in_workspace(
244                                dep.as_ref(),
245                                workspace,
246                            )?;
247                            // Prefer the linkage requested by the package, but defer to the
248                            // workspace if one is not specified at the package level. Use the
249                            // default linkage mode if non is specified
250                            let linkage = dependency
251                                .linkage
252                                .as_deref()
253                                .copied()
254                                .or(dep.linkage.as_deref().copied())
255                                .unwrap_or_default();
256                            dependencies.push(Dependency::new(dep.name.clone(), version, linkage));
257                        },
258                        None => {
259                            return Err(ProjectFileError::InvalidPackageDependency {
260                                source_file: source,
261                                label: Label::new(
262                                    dependency.span(),
263                                    format!("'{}' is not a workspace dependency", &dependency.name),
264                                ),
265                            }
266                            .into());
267                        },
268                    }
269                } else {
270                    return Err(ProjectFileError::InvalidPackageDependency {
271                        source_file: source,
272                        label: Label::new(dependency.span(), "this package is not in a workspace"),
273                    }
274                    .into());
275                }
276            } else {
277                let linkage = dependency.linkage.as_deref().copied().unwrap_or_default();
278                dependencies.push(Dependency::new(
279                    dependency.name.clone(),
280                    DependencyVersionScheme::try_from(dependency.as_ref())?,
281                    linkage,
282                ));
283            }
284        }
285
286        Ok(dependencies)
287    }
288
289    pub fn extract_library_target(&self) -> Result<Option<Span<crate::Target>>, Report> {
290        use miden_assembly_syntax::Path as MasmPath;
291
292        use crate::TargetType;
293
294        if self.lib.is_none() && self.bins.is_empty() {
295            let project_name = &self.package.name;
296            let span = project_name.span();
297            let namespace: Span<Arc<MasmPath>> =
298                Span::new(span, MasmPath::new(project_name.inner()).to_absolute().into());
299            let name = project_name.clone();
300            return Ok(Some(Span::new(
301                span,
302                crate::Target {
303                    ty: TargetType::Library,
304                    name,
305                    namespace,
306                    path: Some(Span::new(span, Uri::new("mod.masm"))),
307                },
308            )));
309        }
310
311        let Some(lib) = self.lib.as_ref() else {
312            return Ok(None);
313        };
314
315        let kind = lib.kind.as_deref().copied().unwrap_or(TargetType::Library);
316        let name = lib
317            .namespace
318            .clone()
319            .unwrap_or_else(|| Span::new(lib.span(), self.package.name.inner().clone()));
320        let namespace = match kind {
321            TargetType::Kernel => Span::new(lib.span(), MasmPath::kernel_path().into()),
322            _ => {
323                let ns = lib
324                    .namespace
325                    .clone()
326                    .unwrap_or_else(|| Span::new(lib.span(), self.package.name.inner().clone()));
327                ns.map(|ns| MasmPath::new(&ns).to_absolute().into())
328            },
329        };
330        Ok(Some(Span::new(
331            lib.span(),
332            crate::Target {
333                ty: kind,
334                name,
335                namespace,
336                path: lib.path.clone(),
337            },
338        )))
339    }
340
341    pub fn extract_executable_targets(&self) -> Vec<Span<crate::Target>> {
342        use miden_assembly_syntax::Path as MasmPath;
343
344        use crate::TargetType;
345
346        let mut bins = Vec::with_capacity(self.bins.len());
347        for target in self.bins.iter() {
348            let span = target.span();
349            let name = target
350                .name
351                .clone()
352                .unwrap_or_else(|| Span::new(target.span(), self.package.name.inner().clone()));
353            let namespace = Span::new(target.span(), Arc::from(MasmPath::exec_path()));
354            bins.push(Span::new(
355                span,
356                crate::Target {
357                    ty: TargetType::Executable,
358                    name,
359                    namespace,
360                    path: target.path.clone(),
361                },
362            ));
363        }
364
365        bins
366    }
367}
368
369impl SetSourceId for ProjectFile {
370    fn set_source_id(&mut self, source_id: SourceId) {
371        let Self {
372            source_file: _,
373            package,
374            config,
375            lib,
376            bins,
377            profiles,
378        } = self;
379        package.set_source_id(source_id);
380        config.set_source_id(source_id);
381        if let Some(lib) = lib.as_mut() {
382            lib.set_source_id(source_id);
383        }
384        bins.set_source_id(source_id);
385        profiles.set_source_id(source_id);
386    }
387}
388
389/// An internal error type for representing information about build target conflicts
390#[derive(Debug, thiserror::Error, Diagnostic)]
391#[error("build target conflicts found")]
392struct TargetConflictError {
393    #[label]
394    label: Label,
395    #[label(collection)]
396    conflicts: Vec<Label>,
397}
398
399impl Validate for ProjectFile {
400    fn validate(&self, source: Arc<SourceFile>) -> Result<(), Report> {
401        use miden_assembly_syntax::ast;
402
403        // Validate the project
404        // 1. Package name must be a valid identifier
405        ast::Ident::validate(&self.package.name).map_err(|err| {
406            Report::from(ProjectFileError::InvalidProjectName {
407                source_file: source.clone(),
408                label: Label::new(self.package.name.span(), err.to_string()),
409            })
410        })?;
411
412        // 2. All build targets must have unique paths (if present) and names (and namespaces must
413        //    be valid)
414        let mut invalid_config = Vec::<RelatedError>::default();
415
416        let mut target_paths = BTreeMap::<Span<Uri>, Option<TargetConflictError>>::default();
417        let mut target_names = BTreeMap::<Span<Arc<str>>, Option<TargetConflictError>>::default();
418        if let Some(lib) = self.lib.as_ref() {
419            if let Some(kind) = lib.kind.as_ref()
420                && !kind.is_library()
421            {
422                invalid_config.push(RelatedError::wrap(RelatedLabel::error("invalid library target")
423                    .with_labeled_span(kind.span(), "this is not a valid target type for a library")
424                    .with_help("Library targets may only be of kind 'library', 'kernel', 'account-component', 'note-script', or 'tx-script'")
425                    .with_source_file(Some(source.clone()))));
426            }
427            if let Some(path) = lib.path.clone() {
428                target_paths.insert(path, None);
429            }
430        }
431
432        for target in self.bins.iter() {
433            use alloc::collections::btree_map::Entry;
434
435            // 2a. Check for conflicting paths
436            let span = target.span();
437            if let Some(path) = target.path.clone() {
438                match target_paths.entry(path) {
439                    Entry::Vacant(entry) => {
440                        entry.insert(None);
441                    },
442                    Entry::Occupied(mut entry) => {
443                        let path_span = target.path.as_ref().map(|p| p.span()).unwrap_or(span);
444                        let conflict_label = Label::new(path_span, "conflict occurs here");
445                        let path = entry.key().clone();
446                        match entry.get_mut() {
447                            Some(error) => {
448                                error.conflicts.push(conflict_label);
449                            },
450                            opt => {
451                                let label = Label::new(
452                                    path.span(),
453                                    format!(
454                                        "the path for this target, `{path}`, conflicts with other targets"
455                                    ),
456                                );
457                                let conflicts = vec![conflict_label];
458                                *opt = Some(TargetConflictError { label, conflicts });
459                            },
460                        }
461                    },
462                }
463            }
464
465            // 2b. Check for name conflicts
466            let name = target
467                .name
468                .clone()
469                .unwrap_or_else(|| Span::new(target.span(), self.package.name.inner().clone()));
470            match target_names.entry(name) {
471                Entry::Vacant(entry) => {
472                    entry.insert(None);
473                },
474                Entry::Occupied(mut entry) => {
475                    let ns_span = target.name.as_ref().map(|ns| ns.span()).unwrap_or(span);
476                    let conflict_label = Label::new(ns_span, "conflict occurs here");
477                    let ns = entry.key().clone();
478                    match entry.get_mut() {
479                        Some(error) => {
480                            error.conflicts.push(conflict_label);
481                        },
482                        opt => {
483                            let label = Label::new(
484                                ns.span(),
485                                format!(
486                                    "the name for this target, `{ns}`, conflicts with other targets"
487                                ),
488                            );
489                            let conflicts = vec![conflict_label];
490                            *opt = Some(TargetConflictError { label, conflicts });
491                        },
492                    }
493                },
494            }
495        }
496
497        invalid_config.extend(target_paths.into_values().flatten().map(RelatedError::wrap));
498        invalid_config.extend(target_names.into_values().flatten().map(RelatedError::wrap));
499
500        if !invalid_config.is_empty() {
501            return Err(ProjectFileError::InvalidBuildTargets {
502                source_file: source.clone(),
503                related: invalid_config,
504            }
505            .into());
506        }
507
508        Ok(())
509    }
510}