Skip to main content

miden_project/
ast.rs

1//! This module and its children define the abstract syntax tree representation of the
2//! `miden-project.toml` file and its variants (i.e. workspace-level vs package-level).
3//!
4//! The AST is used for parsing and rendering the TOML representation, but after validation and
5//! resolution of inherited properties, the AST is translated to a simpler structure that does not
6//! need to represent the complexity of the on-disk format.
7mod dependency;
8mod package;
9pub(crate) mod parsing;
10mod profile;
11mod target;
12#[cfg(all(test, feature = "std", feature = "serde"))]
13mod tests;
14mod workspace;
15
16use alloc::{
17    boxed::Box,
18    format,
19    string::{String, ToString},
20    sync::Arc,
21    vec,
22    vec::Vec,
23};
24
25#[cfg(feature = "serde")]
26use serde::{Deserialize, Serialize};
27
28pub use self::{
29    dependency::DependencySpec,
30    package::{PackageConfig, PackageDetail, ProjectFile},
31    profile::Profile,
32    target::{BinTarget, LibTarget},
33    workspace::WorkspaceFile,
34};
35use crate::{Diagnostic, Label, RelatedError, Report, SourceFile, SourceSpan, miette};
36
37/// Represents all possible variants of `miden-project.toml`
38#[derive(Debug)]
39#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
40#[cfg_attr(feature = "serde", serde(untagged, rename_all = "lowercase"))]
41pub enum MidenProject {
42    /// A workspace-level configuration file.
43    ///
44    /// On its own, a workspace-level `miden-project.toml` does define a package, instead packages
45    /// are derived from the members of the workspace.
46    Workspace(Box<WorkspaceFile>),
47    /// A package-level configuration file.
48    ///
49    /// A `miden-project.toml` of this variety defines a package, and may reference/override any
50    /// workspace-level dependencies, lints, or build profiles.
51    Package(Box<ProjectFile>),
52}
53
54/// Accessors
55impl MidenProject {
56    /// Returns true if this project is actually a multi-project workspace
57    pub fn is_workspace(&self) -> bool {
58        matches!(self, Self::Workspace(_))
59    }
60}
61
62/// Parsing
63#[cfg(feature = "serde")]
64impl MidenProject {
65    /// Parse a [MidenProject] from the provided TOML source file, generally `miden-project.toml`
66    ///
67    /// If successful, the contents of the manifest are semantically valid, with the following
68    /// caveats:
69    ///
70    /// * If parsing a workspace-level configuration, the workspace members are not checked, so it
71    ///   is up to the caller to iterate over the member paths, and parse/validate their respective
72    ///   configurations.
73    /// * If parsing an individual project configuration which belongs to a workspace, inherited
74    ///   properties from the workspace-level are assumed to exist and be correct. It is up to the
75    ///   caller to compute the concrete property values and validate them at that point.
76    pub fn parse(source: Arc<SourceFile>) -> Result<Self, Report> {
77        // We end up parsing the file twice here, which is wasteful, but since these files are
78        // small its of negligable impact, and this is a bit less fragile than searching for
79        // `[workspace]` in the source text.
80        let toml = toml::from_str::<toml::Table>(source.as_str()).map_err(|err| {
81            let span = err
82                .span()
83                .map(|span| {
84                    let start = span.start as u32;
85                    let end = span.end as u32;
86                    SourceSpan::new(source.id(), start..end)
87                })
88                .unwrap_or_default();
89            Report::from(ProjectFileError::ParseError {
90                message: err.message().to_string(),
91                source_file: source.clone(),
92                span,
93            })
94        })?;
95        if toml.contains_key("workspace") {
96            Ok(Self::Workspace(Box::new(WorkspaceFile::parse(source)?)))
97        } else {
98            Ok(Self::Package(Box::new(ProjectFile::parse(source)?)))
99        }
100    }
101}
102
103/// An internal error type used when parsing a `miden-project.toml` file.
104#[allow(dead_code)] // Different feature combinations may produce dead variants
105#[derive(Debug, thiserror::Error, Diagnostic)]
106pub(crate) enum ProjectFileError {
107    #[error("unable to parse project manifest: {message}")]
108    ParseError {
109        message: String,
110        #[source_code]
111        source_file: Arc<SourceFile>,
112        #[label(primary)]
113        span: SourceSpan,
114    },
115    #[error("invalid project name")]
116    #[diagnostic(help("The project name must be a valid Miden Assembly namespace identifier"))]
117    InvalidProjectName {
118        #[source_code]
119        source_file: Arc<SourceFile>,
120        #[label(primary)]
121        label: Label,
122    },
123    #[error("invalid workspace dependency specification")]
124    InvalidWorkspaceDependency {
125        #[source_code]
126        source_file: Arc<SourceFile>,
127        #[label(primary)]
128        label: Label,
129    },
130    #[error("invalid dependency specification")]
131    InvalidPackageDependency {
132        #[source_code]
133        source_file: Arc<SourceFile>,
134        #[label(primary)]
135        label: Label,
136    },
137    #[error("invalid build target configuration")]
138    InvalidBuildTargets {
139        #[source_code]
140        source_file: Arc<SourceFile>,
141        #[related]
142        related: Vec<RelatedError>,
143    },
144    #[error("package is not a member of a workspace")]
145    NotAWorkspace {
146        #[source_code]
147        source_file: Arc<SourceFile>,
148        #[label(primary)]
149        span: SourceSpan,
150    },
151    #[error("failed to load workspace member: {}", span.label().unwrap_or("unknown"))]
152    LoadWorkspaceMemberFailed {
153        #[source_code]
154        source_file: Arc<SourceFile>,
155        #[label(primary)]
156        span: Label,
157    },
158    #[error("no profile named '{name}' has been defined yet")]
159    UnknownProfile {
160        name: Arc<str>,
161        #[source_code]
162        source_file: Arc<SourceFile>,
163        #[label(primary)]
164        span: SourceSpan,
165    },
166    #[error("cannot redefine profile '{name}'")]
167    DuplicateProfile {
168        name: Arc<str>,
169        #[source_code]
170        source_file: Arc<SourceFile>,
171        #[label(primary)]
172        span: SourceSpan,
173        #[label]
174        prev: SourceSpan,
175    },
176    #[error("missing required field 'version'")]
177    MissingVersion {
178        #[source_code]
179        source_file: Arc<SourceFile>,
180        #[label(primary)]
181        span: SourceSpan,
182    },
183    #[error("workspace does not define 'version'")]
184    MissingWorkspaceVersion {
185        #[source_code]
186        source_file: Arc<SourceFile>,
187        #[label(primary)]
188        span: SourceSpan,
189    },
190}