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#[derive(Debug, Clone)]
14#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
15#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
16pub struct PackageTable {
17 pub name: Span<Arc<str>>,
19 #[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#[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 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
39 pub version: Option<Span<MaybeInherit<SemVer>>>,
40 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
42 pub description: Option<Span<MaybeInherit<Arc<str>>>>,
43 #[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#[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 #[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 #[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#[derive(Debug, Clone)]
91#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
92#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
93pub struct ProjectFile {
94 #[cfg_attr(feature = "serde", serde(skip, default))]
96 pub source_file: Option<Arc<SourceFile>>,
97 pub package: PackageTable,
99 #[cfg_attr(feature = "serde", serde(flatten))]
102 pub config: PackageConfig,
103 #[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "Option::is_none"))]
105 pub lib: Option<Span<LibTarget>>,
106 #[cfg_attr(
108 feature = "serde",
109 serde(default, rename = "bin", skip_serializing_if = "Vec::is_empty")
110 )]
111 pub bins: Vec<Span<BinTarget>>,
112 #[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#[cfg(feature = "serde")]
127impl ProjectFile {
128 pub fn parse(source: Arc<SourceFile>) -> Result<Self, Report> {
136 use parsing::{SetSourceId, Validate};
137
138 let source_id = source.id();
139
140 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 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#[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 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 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 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 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}