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;
30pub mod path;
31pub mod registry;
32#[cfg(feature = "std")]
33mod statistics;
34
35use alloc::{boxed::Box, fmt, sync::Arc};
36
37pub const MIDENC_BUILD_VERSION: &str = env!("MIDENC_BUILD_VERSION");
39
40pub const MIDENC_BUILD_REV: &str = env!("MIDENC_BUILD_REV");
42
43use heck::ToKebabCase;
44pub use miden_assembly_syntax;
45pub use miden_mast_package::PackageId;
46pub use miden_package_registry;
47pub use miden_project;
48use midenc_hir_symbol::Symbol;
49
50pub use self::{
51 color::ColorChoice,
52 diagnostics::{DiagnosticsHandler, Emitter, Report, SourceManager},
53 emit::{Emit, Writer},
54 flags::{ArgMatches, CompileFlag, CompileFlags, FlagAction},
55 inputs::{FileName, FileType, InputFile, InputType, InvalidInputError},
56 libs::{LibraryPath, LibraryPathComponent, LinkLibrary, STDLIB, add_target_link_libraries},
57 options::*,
58 outputs::{OutputFile, OutputFiles, OutputMode, OutputType, OutputTypeSpec, OutputTypes},
59 path::{Path, PathBuf},
60};
61#[cfg(feature = "std")]
62pub use self::{duration::HumanDuration, emit::EmitExt, statistics::Statistics};
63
64#[derive(Clone)]
67pub struct Session {
68 pub name: String,
70 pub options: Box<Options>,
72 pub source_manager: Arc<dyn SourceManager + Send + Sync>,
74 pub diagnostics: Arc<DiagnosticsHandler>,
76 pub input: Option<InputFile>,
78 pub output_files: OutputFiles,
80 pub project: miden_project::Project,
84 #[cfg(feature = "std")]
86 pub statistics: Statistics,
87}
88
89impl fmt::Debug for Session {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 f.debug_struct("Session")
92 .field("name", &self.name)
93 .field("options", &self.options)
94 .field("inputs", &self.input)
95 .field("output_files", &self.output_files)
96 .finish_non_exhaustive()
97 }
98}
99
100impl Session {
101 pub fn new(
102 input: InputFile,
103 mut options: Box<Options>,
104 emitter: Option<Arc<dyn Emitter>>,
105 source_manager: Arc<dyn SourceManager + Send + Sync>,
106 ) -> Result<Self, Report> {
107 use miden_debug_types::Span;
108
109 if matches!(input.file_type(), FileType::Toml) {
110 let (pkgid, project) = match &input.file {
111 InputType::Real(path) => {
112 let is_cargo_project =
113 path.file_name().unwrap().eq_ignore_ascii_case("Cargo.toml");
114 let project_path = if is_cargo_project {
115 path.with_file_name("miden-project.toml")
116 } else {
117 path.clone()
118 };
119 let project = miden_project::Project::load(&project_path, &source_manager)
120 .map_err(|err| {
121 err.wrap_err(format!(
122 "failed to load Miden project from {}",
123 project_path.display()
124 ))
125 })?;
126 if options.target_type.is_none() {
127 let project_package = project.package();
128 let target_type = match project_package.library_target() {
129 Some(lib) => lib.ty,
130 None => miden_project::TargetType::Executable,
131 };
132 options.target_type = Some(target_type);
133 }
134 let is_executable_target =
135 options.target_type.is_some_and(|ty| ty.is_executable());
136 let project = {
137 let package = project.package();
138 let has_virtual_executable_target =
139 package.executable_targets().iter().any(|target| {
140 target
141 .path
142 .as_deref()
143 .is_some_and(|path| path.as_str() == "<virtual>")
144 });
145
146 if has_virtual_executable_target
147 || (is_cargo_project && is_executable_target)
148 {
149 miden_project::Project::Package(fixup_targets(
152 package,
153 is_cargo_project && is_executable_target,
154 ))
155 } else {
156 project
157 }
158 };
159 let pkgid = match &project {
160 miden_project::Project::Package(pkg)
161 | miden_project::Project::WorkspacePackage { package: pkg, .. } => {
162 pkg.name().inner().clone()
163 }
164 };
165 (pkgid, project)
166 }
167 InputType::Stdin { name, input } => {
168 let content = core::str::from_utf8(input).map_err(|err| {
169 Report::msg(format!(
170 "unable to load source file '{name}' due to invalid utf-8: {err}"
171 ))
172 })?;
173 let source_file = source_manager.load(
174 miden_debug_types::SourceLanguage::Other("toml"),
175 miden_debug_types::Uri::new(name.as_str()),
176 content.to_string(),
177 );
178 let package = miden_project::Package::load(source_file)?;
179 let pkgid = package.name().inner().clone();
180 (pkgid, miden_project::Project::Package(package.into()))
181 }
182 };
183 let name = options.name.clone().unwrap_or_else(|| pkgid.to_string());
184 if options.target_type.is_none() {
185 let project_package = project.package();
186 let target_type = match project_package.library_target() {
187 Some(lib) => lib.ty,
188 None => miden_project::TargetType::Executable,
189 };
190 options.target_type = Some(target_type);
191 }
192 if is_cargo_project_input(&input) {
193 infer_cargo_project_entrypoint(&project, &mut options)?;
194 }
195 Ok(Self::new_project(name, Some(input), project, options, emitter, source_manager))
196 } else {
197 let name = options
198 .name
199 .clone()
200 .or_else(|| {
201 log::debug!(target: "driver", "no name specified, attempting to derive from output file");
202 options.output_file.as_ref().and_then(|of| of.filestem().map(|stem| stem.to_string()))
203 })
204 .unwrap_or_else(|| {
205 log::debug!(target: "driver", "unable to derive name from output file, deriving from input");
206 match &input {
207 InputFile {
208 file: InputType::Real(path),
209 ..
210 } => path
211 .file_stem()
212 .and_then(|stem| stem.to_str())
213 .or_else(|| path.extension().and_then(|stem| stem.to_str()))
214 .unwrap_or_else(|| {
215 panic!(
216 "invalid input path: '{}' has no file stem or extension",
217 path.display()
218 )
219 })
220 .to_string(),
221 input @ InputFile {
222 file: InputType::Stdin { name, .. },
223 ..
224 } => {
225 let name = name.as_str();
226 if matches!(name, "empty" | "stdin") {
227 log::debug!(target: "driver", "no good input file name to use, using current directory base name");
228 options
229 .current_dir
230 .file_stem()
231 .and_then(|stem| stem.to_str())
232 .unwrap_or(name)
233 .to_string()
234 } else {
235 input.filestem().to_owned()
236 }
237 }
238 }
239 });
240 log::debug!(target: "driver", "artifact name set to '{name}'");
241
242 let mut default_target = miden_project::Target::r#virtual(
243 options.target_type.unwrap_or_default(),
244 name.clone(),
245 miden_assembly_syntax::Path::new(name.as_str()).to_absolute().into_owned(),
246 );
247 if let InputType::Real(path) = &input.file {
248 default_target.path = Some(Span::unknown(miden_project::Uri::from(path.as_path())));
249
250 #[cfg(feature = "std")]
251 {
252 let tmp = std::env::temp_dir().canonicalize().unwrap();
253 let project_dir = tmp.join(&name).join("src");
254 let project_remap_target = if path.is_absolute() {
255 Some(
256 path.strip_prefix(&options.current_dir)
257 .ok()
258 .or(path.as_path().parent())
259 .unwrap()
260 .to_path_buf()
261 .into_boxed_path(),
262 )
263 } else {
264 path.parent().map(|p| p.to_path_buf().into_boxed_path())
265 };
266 options.remap_path_prefixes.push(RemapPathPrefix {
267 from: project_dir.into_boxed_path(),
268 to: project_remap_target,
269 });
270 }
271 }
272 let package = miden_project::Package::new(name.clone(), default_target);
273
274 let package = package.with_dependencies([miden_project::Dependency::new(
276 Span::unknown("miden-core".to_string().into()),
277 miden_project::DependencyVersionScheme::Registry(
278 miden_project::VersionRequirement::Semantic(Span::unknown(
279 miden_project::VersionReq::STAR.clone(),
280 )),
281 ),
282 miden_project::Linkage::Dynamic,
283 )]);
284
285 let project = miden_project::Project::Package(package.into());
286 Ok(Self::new_project(name, Some(input), project, options, emitter, source_manager))
287 }
288 }
289
290 #[allow(clippy::too_many_arguments)]
291 pub fn new_project(
292 name: String,
293 input: Option<InputFile>,
294 project: miden_project::Project,
295 mut options: Box<Options>,
296 emitter: Option<Arc<dyn Emitter>>,
297 source_manager: Arc<dyn SourceManager + Send + Sync>,
298 ) -> Self {
299 log::debug!(target: "driver", "creating session {name}");
300 if log::log_enabled!(target: "driver", log::Level::Debug) {
301 if let Some(input) = input.as_ref() {
302 log::debug!(
303 target: "driver",
304 " | input = {} ({})",
305 input.file_name(),
306 input.file_type(),
307 );
308 }
309 log::debug!(
310 target: "driver",
311 " | outputs_dir = {}",
312 options.output_dir
313 .as_ref()
314 .map(|p| p.display().to_string())
315 .unwrap_or("<unset>".to_string())
316 );
317 log::debug!(
318 target: "driver",
319 " | output_file = {}",
320 options.output_file.as_ref().map(|of| of.to_string()).unwrap_or("<unset>".to_string())
321 );
322 log::debug!(target: "driver", " | target_dir = {}", options.target_dir.display());
323 }
324 let diagnostics = Arc::new(DiagnosticsHandler::new(
325 options.diagnostics,
326 source_manager.clone(),
327 emitter.unwrap_or_else(|| options.default_emitter()),
328 ));
329
330 let output_dir = options
331 .output_dir
332 .as_deref()
333 .or_else(|| options.output_file.as_ref().and_then(|of| of.parent()))
334 .map(|path| path.to_path_buf());
335
336 if let Some(output_dir) = output_dir.as_deref() {
337 log::debug!(target: "driver", " | output dir = {}", output_dir.display());
338 } else {
339 log::debug!(target: "driver", " | output dir = <unset>");
340 }
341
342 log::debug!(target: "driver", " | target = {}", options.target_type.map(|tt| tt.to_string()).unwrap_or("none specified".to_string()));
343 if log::log_enabled!(target: "driver", log::Level::Debug) {
344 for lib in options.link_libraries.iter() {
345 if let Some(path) = lib.path.as_deref() {
346 log::debug!(target: "driver", " | linking library '{}' from {}", &lib.name, path.display());
347 } else {
348 log::debug!(target: "driver", " | linking library '{}'", &lib.name);
349 }
350 }
351 }
352
353 let output_files = OutputFiles::new(
354 name.clone(),
355 options.current_dir.clone(),
356 options.output_dir.clone().unwrap_or_else(|| options.current_dir.clone()),
357 options.output_file.clone(),
358 options.target_dir.clone(),
359 options.output_types.clone(),
360 );
361
362 create_target_dir(options.target_dir.as_path());
363
364 let requires_protocol = options.target_requires_protocol();
366 add_target_link_libraries(&mut options.link_libraries, requires_protocol);
367
368 Self {
369 name,
370 options,
371 source_manager,
372 diagnostics,
373 input,
374 output_files,
375 project,
376 #[cfg(feature = "std")]
377 statistics: Default::default(),
378 }
379 }
380
381 #[doc(hidden)]
382 pub fn with_output_type(mut self, ty: OutputType, path: Option<OutputFile>) -> Self {
383 self.output_files.outputs.insert(ty, path.clone());
384 self.options.output_types.insert(ty, path.clone());
385 self
386 }
387
388 #[doc(hidden)]
389 pub fn with_extra_flags(mut self, flags: CompileFlags) -> Self {
390 self.options.set_extra_flags(flags);
391 self
392 }
393
394 #[inline]
396 pub fn get_flag(&self, name: &str) -> bool {
397 self.options.flags.get_flag(name)
398 }
399
400 #[inline]
402 pub fn get_flag_count(&self, name: &str) -> usize {
403 self.options.flags.get_flag_count(name)
404 }
405
406 #[inline]
408 pub fn matches(&self) -> &ArgMatches {
409 self.options.flags.matches()
410 }
411
412 pub fn name(&self) -> &str {
414 &self.name
415 }
416
417 pub fn package_registry(&self) -> Result<Box<registry::HybridPackageRegistry>, Report> {
419 registry::HybridPackageRegistry::new(&self.options).map(Box::new)
420 }
421
422 pub fn out_file(&self) -> OutputFile {
424 let out_file = self.output_files.output_file(OutputType::Masp, None);
425
426 if let OutputFile::Real(ref path) = out_file {
427 self.check_file_is_writeable(path);
428 }
429
430 out_file
431 }
432
433 #[cfg(not(feature = "std"))]
434 fn check_file_is_writeable(&self, file: &Path) {
435 panic!(
436 "Compiler exited with a fatal error: cannot write '{}' - compiler was built without \
437 standard library",
438 file.display()
439 );
440 }
441
442 #[cfg(feature = "std")]
443 fn check_file_is_writeable(&self, file: &Path) {
444 if let Ok(m) = file.metadata()
445 && m.permissions().readonly()
446 {
447 panic!("Compiler exited with a fatal error: file is not writeable: {}", file.display());
448 }
449 }
450
451 pub fn parse_only(&self) -> bool {
453 self.options.parse_only
454 }
455
456 pub fn analyze_only(&self) -> bool {
458 self.options.analyze_only
459 }
460
461 pub fn rewrite_only(&self) -> bool {
463 let link_or_masm_requested = self.should_link() || self.should_codegen();
464 !self.options.parse_only && !self.options.analyze_only && !link_or_masm_requested
465 }
466
467 pub fn should_link(&self) -> bool {
469 self.options.output_types.should_link() && !self.options.no_link
470 }
471
472 pub fn should_codegen(&self) -> bool {
474 self.options.output_types.should_codegen() && !self.options.link_only
475 }
476
477 pub fn should_assemble(&self) -> bool {
479 self.options.output_types.should_assemble() && !self.options.link_only
480 }
481
482 pub fn should_emit(&self, ty: OutputType) -> bool {
484 self.options.output_types.contains_key(&ty)
485 }
486
487 pub fn should_print_ir(&self, pass: &str) -> bool {
489 self.options.print_ir_after_all
490 || self.options.print_ir_after_pass.iter().any(|p| p == pass)
491 }
492
493 pub fn should_print_ir_before_stage(&self, stage: &str) -> bool {
495 self.options.print_ir_before_stage.iter().any(|s| s == stage)
496 }
497
498 pub fn should_print_cfg(&self, pass: &str) -> bool {
500 self.options.print_cfg_after_all
501 || self.options.print_cfg_after_pass.iter().any(|p| p == pass)
502 }
503
504 #[cfg(feature = "std")]
506 pub fn print(&self, ir: impl Emit, pass: &str) -> anyhow::Result<()> {
507 if self.should_print_ir(pass) {
508 ir.write_to_stdout(self)?;
509 }
510 Ok(())
511 }
512
513 pub fn emit_to(&self, ty: OutputType, name: Option<Symbol>) -> Option<PathBuf> {
515 if self.should_emit(ty) {
516 match self.output_files.output_file(ty, name.map(|n| n.as_str())) {
517 OutputFile::Real(path) => Some(path),
518 OutputFile::Directory(_) => {
519 unreachable!("OutputFiles::output_file never returns OutputFile::Directory")
520 }
521 OutputFile::Stdout => None,
522 }
523 } else {
524 None
525 }
526 }
527
528 #[cfg(feature = "std")]
530 pub fn emit<E: Emit>(&self, mode: OutputMode, item: &E) -> anyhow::Result<()> {
531 let output_type = item.output_type(mode);
532 if self.should_emit(output_type) {
533 let name = item.name().map(|n| n.as_str());
534 match self.output_files.output_file(output_type, name) {
535 OutputFile::Real(path) => {
536 item.write_to_file(&path, mode, self)?;
537 }
538 OutputFile::Directory(_) => {
539 unreachable!("OutputFiles::output_file never returns OutputFile::Directory")
540 }
541 OutputFile::Stdout => {
542 let stdout = std::io::stdout().lock();
543 item.write_to(stdout, mode, self)?;
544 }
545 }
546 }
547
548 Ok(())
549 }
550
551 #[cfg(not(feature = "std"))]
552 pub fn emit<E: Emit>(&self, _mode: OutputMode, _item: &E) -> anyhow::Result<()> {
553 Ok(())
554 }
555}
556
557fn is_cargo_project_input(input: &InputFile) -> bool {
558 matches!(
559 &input.file,
560 InputType::Real(path) if path.file_name().is_some_and(|name| name.eq_ignore_ascii_case("Cargo.toml"))
561 )
562}
563
564fn infer_cargo_project_entrypoint(
565 project: &miden_project::Project,
566 options: &mut Options,
567) -> Result<(), Report> {
568 if options.entrypoint.is_some() {
569 return Ok(());
570 }
571
572 match options.target_type {
573 Some(miden_project::TargetType::Executable) => {
574 let package = project.package();
575 let targets = package.executable_targets();
576 let target = if let Some(target_name) = options.target.as_deref() {
577 targets.iter().find(|target| target_name == &**target.name.inner()).ok_or_else(
578 || Report::msg(format!("no executable target name '{target_name}'")),
579 )?
580 } else if targets.len() == 1 {
581 &targets[0]
582 } else {
583 return Err(Report::msg(
584 "ambiguous executable target selection: use --target to select a specific \
585 executable target",
586 ));
587 };
588
589 let masm_module_name = target.name.inner().replace('-', "_");
590 options.entrypoint = Some(format!("{masm_module_name}::entrypoint"));
591 }
592 Some(miden_project::TargetType::TransactionScript) => {
593 options.entrypoint = Some("miden:base/transaction-script@1.0.0::run".to_string());
594 }
595 _ => (),
596 }
597
598 Ok(())
599}
600
601pub fn fixup_targets(
602 package: Arc<miden_project::Package>,
603 is_cargo_project: bool,
604) -> Arc<miden_project::Package> {
605 let requires_virtual_rewrite = package
609 .executable_targets()
610 .iter()
611 .any(|t| t.path.as_deref().is_some_and(|path| path.as_str() == "<virtual>"));
612 if requires_virtual_rewrite || is_cargo_project {
613 let mut prev_targets = package.executable_targets().iter();
615 let mut default_target = match package.library_target().cloned() {
616 Some(target) => target.inner().clone(),
617 None => prev_targets.next().unwrap().inner().clone(),
618 };
619 if is_cargo_project {
620 rewrite_component_target_namespace(&mut default_target, &package);
621 }
622 if default_target.path.as_deref().is_some_and(|p| p.as_str() == "<virtual>") {
623 default_target.path = None;
624 }
625 let new_package = miden_project::Package::new(package.name().into_inner(), default_target)
626 .with_version(package.version().into_inner().clone())
627 .with_dependencies(package.dependencies().iter().cloned())
628 .with_lints(package.lints().clone())
629 .with_metadata(package.metadata().clone())
630 .with_targets(prev_targets.map(|t| {
631 let mut t = t.inner().clone();
632 if t.path.as_deref().is_some_and(|p| p.as_str() == "<virtual>") {
633 t.path = None;
634 } else if is_cargo_project {
635 rewrite_component_target_namespace(&mut t, &package);
636 }
637 t
638 }));
639 let new_package = package
640 .profiles()
641 .iter()
642 .cloned()
643 .fold(new_package, |pkg, profile| pkg.with_profile(profile));
644 new_package.into()
645 } else {
646 package
647 }
648}
649
650fn rewrite_component_target_namespace(
651 target: &mut miden_project::Target,
652 package: &miden_project::Package,
653) {
654 use miden_assembly_syntax::ast;
655 use miden_debug_types::Span;
656
657 let namespace_id = target.namespace.to_relative().as_ident().map(|id| id.into_inner());
658 if target.ty.is_executable()
659 || namespace_id.as_deref().is_none_or(|id| package.name().inner() != id)
660 {
661 return;
662 }
663
664 let component_namespace = package.name().to_kebab_case();
668 let component_id = format!(
669 "::miden:{component_namespace}/miden-{component_namespace}@{}",
670 package.version()
671 );
672 target.namespace = Span::unknown(ast::Path::new(&component_id).into());
673}
674
675#[cfg(feature = "std")]
676fn create_target_dir(path: &Path) {
677 std::fs::create_dir_all(path)
678 .unwrap_or_else(|err| panic!("unable to create --target-dir '{}': {err}", path.display()));
679}
680
681#[cfg(not(feature = "std"))]
682fn create_target_dir(_path: &Path) {}