1#![no_std]
2#![feature(debug_closure_helpers)]
3#![feature(specialization)]
4#![allow(incomplete_features)]
6#![deny(warnings)]
7
8#[macro_use]
9extern crate alloc;
10#[cfg(feature = "std")]
11extern crate std;
12
13use alloc::{
14 borrow::ToOwned,
15 format,
16 string::{String, ToString},
17};
18
19mod color;
20pub mod diagnostics;
21#[cfg(feature = "std")]
22mod duration;
23mod emit;
24mod emitter;
25pub mod flags;
26mod inputs;
27mod libs;
28mod options;
29mod outputs;
30#[cfg(feature = "std")]
31mod package_lease;
32pub mod path;
33pub mod registry;
34#[cfg(feature = "std")]
35mod statistics;
36
37use alloc::{boxed::Box, fmt, sync::Arc};
38
39pub const MIDENC_BUILD_VERSION: &str = env!("MIDENC_BUILD_VERSION");
41
42pub const MIDENC_BUILD_REV: &str = env!("MIDENC_BUILD_REV");
44
45pub use miden_assembly_syntax;
46pub use miden_mast_package::PackageId;
47pub use miden_package_registry;
48pub use miden_project;
49use midenc_hir_symbol::Symbol;
50
51pub use self::{
52 color::ColorChoice,
53 diagnostics::{DiagnosticsHandler, Emitter, Report, SourceManager},
54 emit::{Emit, Writer},
55 flags::{ArgMatches, CompileFlag, CompileFlags, FlagAction},
56 inputs::{FileName, FileType, InputFile, InputType, InvalidInputError},
57 libs::{LibraryPath, LibraryPathComponent, LinkLibrary, add_target_link_libraries},
58 options::*,
59 outputs::{OutputFile, OutputFiles, OutputMode, OutputType, OutputTypeSpec, OutputTypes},
60 path::{Path, PathBuf},
61};
62#[cfg(feature = "std")]
63pub use self::{duration::HumanDuration, emit::EmitExt, statistics::Statistics};
64
65#[derive(Clone)]
68pub struct Session {
69 pub name: String,
71 pub options: Box<Options>,
73 pub source_manager: Arc<dyn SourceManager>,
75 pub diagnostics: Arc<DiagnosticsHandler>,
77 pub input: Option<InputFile>,
79 pub output_files: OutputFiles,
81 #[cfg(feature = "std")]
83 pub statistics: Statistics,
84 #[cfg(feature = "std")]
91 package_cache_lease: package_lease::SharedPackageCacheLease,
92}
93
94impl fmt::Debug for Session {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.debug_struct("Session")
97 .field("name", &self.name)
98 .field("options", &self.options)
99 .field("inputs", &self.input)
100 .field("output_files", &self.output_files)
101 .finish_non_exhaustive()
102 }
103}
104
105impl Session {
106 pub fn new(
135 input: InputFile,
136 mut options: Box<Options>,
137 emitter: Option<Arc<dyn Emitter>>,
138 source_manager: Arc<dyn SourceManager + Send + Sync>,
139 ) -> Result<Self, Report> {
140 let manifest = if matches!(input.file_type(), FileType::Toml) {
141 ProjectManifest::read(&input, source_manager.as_ref())?
142 } else {
143 None
144 };
145
146 if let Some(manifest) = manifest.as_ref() {
147 if options.target_type.is_none() {
148 options.target_type = Some(manifest.library_target_type());
149 }
150 if is_cargo_project_input(&input) {
151 infer_cargo_project_entrypoint(manifest, &mut options)?;
152 }
153 }
154
155 let name = options
156 .name
157 .clone()
158 .or_else(|| manifest.as_ref().map(|manifest| manifest.name.to_string()))
159 .or_else(|| {
160 log::debug!(target: "driver", "no name specified, attempting to derive from output file");
161 options.output_file.as_ref().and_then(|of| of.filestem().map(|stem| stem.to_string()))
162 })
163 .unwrap_or_else(|| {
164 log::debug!(target: "driver", "unable to derive name from output file, deriving from input");
165 match &input {
166 InputFile {
167 file: InputType::Real(path),
168 ..
169 } => path
170 .file_stem()
171 .and_then(|stem| stem.to_str())
172 .or_else(|| path.extension().and_then(|stem| stem.to_str()))
173 .unwrap_or_else(|| {
174 panic!(
175 "invalid input path: '{}' has no file stem or extension",
176 path.display()
177 )
178 })
179 .to_string(),
180 input @ InputFile {
181 file: InputType::Stdin { name, .. },
182 ..
183 } => {
184 let name = name.as_str();
185 if matches!(name, "empty" | "stdin") {
186 log::debug!(target: "driver", "no good input file name to use, using current directory base name");
187 options
188 .current_dir
189 .file_stem()
190 .and_then(|stem| stem.to_str())
191 .unwrap_or(name)
192 .to_string()
193 } else {
194 input.filestem().to_owned()
195 }
196 }
197 }
198 });
199 log::debug!(target: "driver", "artifact name set to '{name}'");
200
201 if !matches!(input.file_type(), FileType::Toml)
206 && let InputType::Real(path) = &input.file
207 {
208 #[cfg(feature = "std")]
209 {
210 let tmp = std::env::temp_dir().canonicalize().unwrap();
211 let project_dir = tmp.join(&name).join("src");
212 let project_remap_target = if path.is_absolute() {
213 Some(
214 path.strip_prefix(&options.current_dir)
215 .ok()
216 .or(path.as_path().parent())
217 .unwrap()
218 .to_path_buf()
219 .into_boxed_path(),
220 )
221 } else {
222 path.parent().map(|p| p.to_path_buf().into_boxed_path())
223 };
224 options.remap_path_prefixes.push(RemapPathPrefix {
225 from: project_dir.into_boxed_path(),
226 to: project_remap_target,
227 });
228 }
229 }
230
231 Ok(Self::new_project(name, Some(input), options, emitter, source_manager))
232 }
233
234 pub fn new_project(
239 name: String,
240 input: Option<InputFile>,
241 mut options: Box<Options>,
242 emitter: Option<Arc<dyn Emitter>>,
243 source_manager: Arc<dyn SourceManager>,
244 ) -> Self {
245 log::debug!(target: "driver", "creating session {name}");
246 if log::log_enabled!(target: "driver", log::Level::Debug) {
247 if let Some(input) = input.as_ref() {
248 log::debug!(
249 target: "driver",
250 " | input = {} ({})",
251 input.file_name(),
252 input.file_type(),
253 );
254 }
255 log::debug!(
256 target: "driver",
257 " | outputs_dir = {}",
258 options.output_dir
259 .as_ref()
260 .map(|p| p.display().to_string())
261 .unwrap_or("<unset>".to_string())
262 );
263 log::debug!(
264 target: "driver",
265 " | output_file = {}",
266 options.output_file.as_ref().map(|of| of.to_string()).unwrap_or("<unset>".to_string())
267 );
268 log::debug!(target: "driver", " | target_dir = {}", options.target_dir.display());
269 }
270 let diagnostics = Arc::new(DiagnosticsHandler::new(
271 options.diagnostics,
272 source_manager.clone(),
273 emitter.unwrap_or_else(|| options.default_emitter()),
274 ));
275
276 let profile_target_dir = options.target_dir.join(&options.profile);
277 create_target_dir(&profile_target_dir);
278
279 let output_dir = options
280 .output_dir
281 .as_deref()
282 .or_else(|| options.output_file.as_ref().and_then(|of| of.parent()))
283 .map(|path| path.to_path_buf())
284 .unwrap_or_else(|| profile_target_dir.clone());
285 create_target_dir(&output_dir);
286
287 log::debug!(target: "driver", " | output dir = {}", output_dir.display());
288 log::debug!(target: "driver", " | target = {}", options.target_type.map(|tt| tt.to_string()).unwrap_or("none specified".to_string()));
289 if log::log_enabled!(target: "driver", log::Level::Debug) {
290 for lib in options.link_libraries.iter() {
291 if let Some(path) = lib.path.as_deref() {
292 log::debug!(target: "driver", " | linking library '{}' from {}", lib.name, path.display());
293 } else {
294 log::debug!(target: "driver", " | linking library '{}'", lib.name);
295 }
296 }
297 }
298
299 let output_files = OutputFiles::new(
300 name.clone(),
301 options.current_dir.clone(),
302 output_dir.clone(),
303 options.output_file.clone(),
304 profile_target_dir.clone(),
305 options.output_types.clone(),
306 );
307
308 let requires_protocol = options.target_requires_protocol();
310 add_target_link_libraries(&mut options.link_libraries, requires_protocol);
311
312 Self {
313 name,
314 options,
315 source_manager,
316 diagnostics,
317 input,
318 output_files,
319 #[cfg(feature = "std")]
320 statistics: Default::default(),
321 #[cfg(feature = "std")]
322 package_cache_lease: Default::default(),
323 }
324 }
325
326 #[doc(hidden)]
327 pub fn with_output_type(mut self, ty: OutputType, path: Option<OutputFile>) -> Self {
328 self.output_files.outputs.insert(ty, path.clone());
329 self.options.output_types.insert(ty, path.clone());
330 self
331 }
332
333 #[doc(hidden)]
334 pub fn with_extra_flags(mut self, flags: CompileFlags) -> Self {
335 self.options.set_extra_flags(flags);
336 self
337 }
338
339 #[inline]
341 pub fn get_flag(&self, name: &str) -> bool {
342 self.options.flags.get_flag(name)
343 }
344
345 #[inline]
347 pub fn get_flag_count(&self, name: &str) -> usize {
348 self.options.flags.get_flag_count(name)
349 }
350
351 #[inline]
353 pub fn matches(&self) -> &ArgMatches {
354 self.options.flags.matches()
355 }
356
357 pub fn name(&self) -> &str {
359 &self.name
360 }
361
362 pub fn package_registry(&self) -> Result<Box<registry::HybridPackageRegistry>, Report> {
364 #[cfg(feature = "std")]
365 let filesystem_cache = self.filesystem_package_cache_dir()?;
366 #[cfg(not(feature = "std"))]
367 let filesystem_cache = None;
368 #[allow(unused_mut)]
369 let mut registry = registry::HybridPackageRegistry::new_with_filesystem_cache(
370 &self.options,
371 filesystem_cache,
372 )?;
373 #[cfg(feature = "std")]
377 registry.retain_session_package_cache(self.package_cache_lease.clone());
378 Ok(Box::new(registry))
379 }
380
381 #[cfg(feature = "std")]
413 pub fn filesystem_package_cache_dir(&self) -> Result<Option<PathBuf>, Report> {
414 if !self.is_project_session() {
415 return Ok(None);
416 }
417 let lease = self
418 .package_cache_lease
419 .get_or_init(|| package_lease::PackageCacheLease::create(&self.options.target_dir));
420 match lease {
421 Ok(lease) => Ok(Some(lease.path().to_path_buf())),
422 Err(message) => Err(Report::msg(message.clone())),
423 }
424 }
425
426 #[cfg(not(feature = "std"))]
428 pub fn filesystem_package_cache_dir(&self) -> Result<Option<PathBuf>, Report> {
429 Ok(None)
430 }
431
432 #[cfg(feature = "std")]
438 fn is_project_session(&self) -> bool {
439 self.input
440 .as_ref()
441 .is_some_and(|input| matches!(input.file_type(), FileType::Toml))
442 }
443
444 pub fn out_file(&self) -> OutputFile {
446 let out_file = self.output_files.output_file(OutputType::Masp, None);
447
448 if let OutputFile::Real(ref path) = out_file {
449 self.check_file_is_writeable(path);
450 }
451
452 out_file
453 }
454
455 #[cfg(not(feature = "std"))]
456 fn check_file_is_writeable(&self, file: &Path) {
457 panic!(
458 "Compiler exited with a fatal error: cannot write '{}' - compiler was built without \
459 standard library",
460 file.display()
461 );
462 }
463
464 #[cfg(feature = "std")]
465 fn check_file_is_writeable(&self, file: &Path) {
466 if let Ok(m) = file.metadata()
467 && m.permissions().readonly()
468 {
469 panic!("Compiler exited with a fatal error: file is not writeable: {}", file.display());
470 }
471 }
472
473 pub fn parse_only(&self) -> bool {
475 self.options.parse_only
476 }
477
478 pub fn analyze_only(&self) -> bool {
480 self.options.analyze_only
481 }
482
483 pub fn rewrite_only(&self) -> bool {
485 let link_or_masm_requested = self.should_link() || self.should_codegen();
486 !self.options.parse_only && !self.options.analyze_only && !link_or_masm_requested
487 }
488
489 pub fn should_link(&self) -> bool {
491 self.options.output_types.should_link() && !self.options.no_link
492 }
493
494 pub fn should_codegen(&self) -> bool {
496 self.options.output_types.should_codegen() && !self.options.link_only
497 }
498
499 pub fn should_assemble(&self) -> bool {
501 self.options.output_types.should_assemble() && !self.options.link_only
502 }
503
504 pub fn should_emit(&self, ty: OutputType) -> bool {
506 self.options.output_types.contains_key(&ty)
507 }
508
509 pub fn should_print_ir(&self, pass: &str) -> bool {
511 self.options.print_ir_after_all
512 || self.options.print_ir_after_pass.iter().any(|p| p == pass)
513 }
514
515 pub fn should_print_ir_before_stage(&self, stage: &str) -> bool {
517 self.options.print_ir_before_stage.iter().any(|s| s == stage)
518 }
519
520 pub fn should_print_cfg(&self, pass: &str) -> bool {
522 self.options.print_cfg_after_all
523 || self.options.print_cfg_after_pass.iter().any(|p| p == pass)
524 }
525
526 #[cfg(feature = "std")]
528 pub fn print(&self, ir: impl Emit, pass: &str) -> anyhow::Result<()> {
529 if self.should_print_ir(pass) {
530 ir.write_to_stdout(self)?;
531 }
532 Ok(())
533 }
534
535 pub fn emit_to(&self, ty: OutputType, name: Option<Symbol>) -> Option<PathBuf> {
537 if self.should_emit(ty) {
538 match self.output_files.output_file(ty, name.map(|n| n.as_str())) {
539 OutputFile::Real(path) => Some(path),
540 OutputFile::Directory(_) => {
541 unreachable!("OutputFiles::output_file never returns OutputFile::Directory")
542 }
543 OutputFile::Stdout => None,
544 }
545 } else {
546 None
547 }
548 }
549
550 #[cfg(feature = "std")]
552 pub fn emit<E: Emit>(&self, mode: OutputMode, item: &E) -> anyhow::Result<()> {
553 let output_type = item.output_type(mode);
554 let name = item.name().map(|n| n.as_str());
555 match self.output_path_for(output_type, name) {
556 Some(OutputFile::Real(path)) => {
557 item.write_to_file(&path, mode, self)?;
558 }
559 Some(OutputFile::Directory(_)) => {
560 unreachable!("OutputFiles::output_file never returns OutputFile::Directory")
561 }
562 Some(OutputFile::Stdout) => {
563 let stdout = std::io::stdout().lock();
564 item.write_to(stdout, mode, self)?;
565 }
566 None => (),
567 }
568
569 Ok(())
570 }
571
572 #[cfg(feature = "std")]
577 pub fn output_path_for(
578 &self,
579 output_type: OutputType,
580 name: Option<&str>,
581 ) -> Option<OutputFile> {
582 if self.should_emit(output_type) {
583 Some(self.output_files.output_file(output_type, name))
584 } else {
585 None
586 }
587 }
588
589 #[cfg(not(feature = "std"))]
590 pub fn emit<E: Emit>(&self, _mode: OutputMode, _item: &E) -> anyhow::Result<()> {
591 Ok(())
592 }
593}
594
595fn is_cargo_project_input(input: &InputFile) -> bool {
596 matches!(
597 &input.file,
598 InputType::Real(path) if path.file_name().is_some_and(|name| name.eq_ignore_ascii_case("Cargo.toml"))
599 )
600}
601
602pub struct ProjectManifest {
625 name: String,
627 library: Option<miden_project::Target>,
629 executables: alloc::vec::Vec<miden_project::Target>,
631}
632
633impl ProjectManifest {
634 pub fn from_package(package: &miden_project::Package) -> Self {
640 Self {
641 name: package.name().to_string(),
642 library: package.library_target().map(|lib| lib.inner().clone()),
643 executables: package
644 .executable_targets()
645 .iter()
646 .map(|bin| bin.inner().clone())
647 .collect(),
648 }
649 }
650
651 pub fn library_target_type(&self) -> miden_project::TargetType {
656 match self.library.as_ref() {
657 Some(library) => library.ty,
658 None => miden_project::TargetType::Executable,
659 }
660 }
661
662 pub fn selected_executable(
668 &self,
669 requested: Option<&str>,
670 ) -> Result<&miden_project::Target, Report> {
671 match requested {
672 Some(name) => self
673 .executables
674 .iter()
675 .find(|target| name == &**target.name.inner())
676 .ok_or_else(|| Report::msg(format!("no executable target name '{name}'"))),
677 None if self.executables.len() == 1 => Ok(&self.executables[0]),
678 None => Err(Report::msg(
679 "ambiguous executable target selection: use --target to select a specific \
680 executable target",
681 )),
682 }
683 }
684
685 fn read(input: &InputFile, source_manager: &dyn SourceManager) -> Result<Option<Self>, Report> {
692 match &input.file {
693 InputType::Real(path) => {
694 let manifest_path =
698 if path.file_name().is_some_and(|name| name.eq_ignore_ascii_case("Cargo.toml"))
699 {
700 path.with_file_name("miden-project.toml")
701 } else {
702 path.clone()
703 };
704 #[cfg(feature = "std")]
705 {
706 use miden_debug_types::SourceManagerExt;
707 let Ok(source) = source_manager.load_file(&manifest_path) else {
708 return Ok(None);
709 };
710 Ok(Self::parse(source).ok())
711 }
712 #[cfg(not(feature = "std"))]
713 {
714 let _ = manifest_path;
715 Ok(None)
716 }
717 }
718 InputType::Stdin { name, input } => {
719 let content = core::str::from_utf8(input).map_err(|err| {
720 Report::msg(format!(
721 "unable to load source file '{name}' due to invalid utf-8: {err}"
722 ))
723 })?;
724 let source_file = source_manager.load(
725 miden_debug_types::SourceLanguage::Other("toml"),
726 miden_debug_types::Uri::new(name.as_str()),
727 content.to_string(),
728 );
729 Self::parse(source_file).map(Some)
730 }
731 }
732 }
733
734 fn parse(source: Arc<diagnostics::SourceFile>) -> Result<Self, Report> {
735 let package = match miden_project::ast::MidenProject::parse(source)? {
736 miden_project::ast::MidenProject::Package(package) => package,
737 miden_project::ast::MidenProject::Workspace(_) => {
742 return Err(Report::msg(
743 "expected a package manifest, but found a workspace manifest",
744 ));
745 }
746 };
747 use miden_debug_types::Span;
750 Ok(Self {
751 name: package.package.name.inner().to_string(),
752 library: package.extract_library_target()?.map(Span::into_inner),
753 executables: package
754 .extract_executable_targets()
755 .into_iter()
756 .map(Span::into_inner)
757 .collect(),
758 })
759 }
760}
761
762fn infer_cargo_project_entrypoint(
763 manifest: &ProjectManifest,
764 options: &mut Options,
765) -> Result<(), Report> {
766 if options.entrypoint.is_some() {
767 return Ok(());
768 }
769
770 match options.target_type {
771 Some(miden_project::TargetType::Executable) => {
772 let target = manifest.selected_executable(options.target.as_deref())?;
773 let masm_module_name = target.name.inner().replace('-', "_");
774 options.entrypoint = Some(format!("{masm_module_name}::entrypoint"));
775 }
776 Some(miden_project::TargetType::TransactionScript) => {
777 options.entrypoint = Some("miden:base/transaction-script@1.0.0::run".to_string());
778 }
779 _ => (),
780 }
781
782 Ok(())
783}
784
785#[cfg(feature = "std")]
786fn create_target_dir(path: &Path) {
787 if !path.exists() {
788 std::fs::create_dir_all(path).unwrap_or_else(|err| {
789 panic!("unable to create --target-dir '{}': {err}", path.display())
790 });
791 }
792}
793
794#[cfg(not(feature = "std"))]
795fn create_target_dir(_path: &Path) {}
796
797#[cfg(test)]
798mod tests {
799 use alloc::sync::Arc;
800
801 use tempfile::TempDir;
802
803 use super::*;
804
805 #[test]
806 fn relative_manifest_locator_uses_the_configured_current_directory() {
807 let temp = TempDir::new().unwrap();
808 let options = Options {
809 current_dir: temp.path().to_path_buf(),
810 target_dir: temp.path().join("target"),
811 ..Options::default()
812 };
813 let input = InputFile::new(FileType::Toml, InputType::Real("Cargo.toml".into()));
814 let session = Session::new_project(
815 "relative-manifest".into(),
816 Some(input),
817 Box::new(options),
818 None,
819 Arc::new(diagnostics::DefaultSourceManager::default()),
820 );
821
822 let cache_dir = session.filesystem_package_cache_dir().unwrap().unwrap();
823 let expected_parent = temp.path().join("target/packages");
825 assert_eq!(cache_dir.parent(), Some(expected_parent.as_path()));
826 assert!(cache_dir.is_dir(), "the lease directory must exist once derived");
827
828 let clone_dir = session.clone().filesystem_package_cache_dir().unwrap().unwrap();
829 assert_eq!(cache_dir, clone_dir, "clones must share one lease, never mint a second");
830
831 drop(session);
832 assert!(!cache_dir.exists(), "dropping the last session must delete the lease");
833 }
834
835 #[test]
836 fn a_registry_keeps_the_leased_cache_alive_after_the_session_drops() {
837 let temp = TempDir::new().unwrap();
838 let options = Options {
839 current_dir: temp.path().to_path_buf(),
840 target_dir: temp.path().join("target"),
841 ..Options::default()
842 };
843 let input = InputFile::new(FileType::Toml, InputType::Real("Cargo.toml".into()));
844 let session = Session::new_project(
845 "registry-outlives".into(),
846 Some(input),
847 Box::new(options),
848 None,
849 Arc::new(diagnostics::DefaultSourceManager::default()),
850 );
851
852 let registry = session.package_registry().unwrap();
853 let cache_dir = registry.filesystem_cache_dir().unwrap().to_path_buf();
854 assert!(cache_dir.is_dir());
855
856 drop(session);
857 assert!(
858 cache_dir.is_dir(),
859 "the registry publishes into the leased directory, so it must keep the lease alive"
860 );
861
862 drop(registry);
863 assert!(!cache_dir.exists(), "dropping the last owner must delete the lease");
864 }
865}