Skip to main content

midenc_session/
lib.rs

1#![no_std]
2#![feature(debug_closure_helpers)]
3#![feature(specialization)]
4// Specialization
5#![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
37/// The version associated with the current compiler toolchain
38pub const MIDENC_BUILD_VERSION: &str = env!("MIDENC_BUILD_VERSION");
39
40/// The git revision associated with the current compiler toolchain
41pub 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/// This struct provides access to all of the metadata and configuration
65/// needed during a single compilation session.
66#[derive(Clone)]
67pub struct Session {
68    /// The name of this session
69    pub name: String,
70    /// Configuration for the current compiler session
71    pub options: Box<Options>,
72    /// The current source manager
73    pub source_manager: Arc<dyn SourceManager + Send + Sync>,
74    /// The current diagnostics handler
75    pub diagnostics: Arc<DiagnosticsHandler>,
76    /// The inputs being compiled
77    pub input: Option<InputFile>,
78    /// The outputs to be produced by the compiler during compilation
79    pub output_files: OutputFiles,
80    /// The project being compiled
81    ///
82    /// This may be a virtual manifest (i.e. materialized only in-memory)
83    pub project: miden_project::Project,
84    /// Statistics gathered from the current compiler session
85    #[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                            // HACK(pauls): Workaround bug with virtual bin targets until
150                            // 0.24.x. See https://github.com/0xMiden/miden-vm/pull/3156
151                            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            // Currently, we always require the core library to be linked
275            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        // Linka against implicitly required libraries
365        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    /// Get the value of a custom flag with action `FlagAction::SetTrue` or `FlagAction::SetFalse`
395    #[inline]
396    pub fn get_flag(&self, name: &str) -> bool {
397        self.options.flags.get_flag(name)
398    }
399
400    /// Get the count of a specific custom flag with action `FlagAction::Count`
401    #[inline]
402    pub fn get_flag_count(&self, name: &str) -> usize {
403        self.options.flags.get_flag_count(name)
404    }
405
406    /// Get the remaining [ArgMatches] left after parsing the base session configuration
407    #[inline]
408    pub fn matches(&self) -> &ArgMatches {
409        self.options.flags.matches()
410    }
411
412    /// The name of this session (used as the name of the project, output file, etc.)
413    pub fn name(&self) -> &str {
414        &self.name
415    }
416
417    /// Get a new package registry instance for this session
418    pub fn package_registry(&self) -> Result<Box<registry::HybridPackageRegistry>, Report> {
419        registry::HybridPackageRegistry::new(&self.options).map(Box::new)
420    }
421
422    /// Get the [OutputFile] to write the assembled MAST output to
423    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    /// Returns true if the compiler should exit after parsing the input
452    pub fn parse_only(&self) -> bool {
453        self.options.parse_only
454    }
455
456    /// Returns true if the compiler should exit after performing semantic analysis
457    pub fn analyze_only(&self) -> bool {
458        self.options.analyze_only
459    }
460
461    /// Returns true if the compiler should exit after applying rewrites to the IR
462    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    /// Returns true if an [OutputType] that requires linking + assembly was requested
468    pub fn should_link(&self) -> bool {
469        self.options.output_types.should_link() && !self.options.no_link
470    }
471
472    /// Returns true if an [OutputType] that requires generating Miden Assembly was requested
473    pub fn should_codegen(&self) -> bool {
474        self.options.output_types.should_codegen() && !self.options.link_only
475    }
476
477    /// Returns true if an [OutputType] that requires assembling MAST was requested
478    pub fn should_assemble(&self) -> bool {
479        self.options.output_types.should_assemble() && !self.options.link_only
480    }
481
482    /// Returns true if the given [OutputType] should be emitted as an output
483    pub fn should_emit(&self, ty: OutputType) -> bool {
484        self.options.output_types.contains_key(&ty)
485    }
486
487    /// Returns true if IR should be printed to stdout, after executing a pass named `pass`
488    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    /// Returns true if IR should be printed to stdout, at the start of `stage`
494    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    /// Returns true if CFG should be printed to stdout, after executing a pass named `pass`
499    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    /// Print the given emittable IR to stdout, as produced by a pass with name `pass`
505    #[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    /// Get the path to emit the given [OutputType] to
514    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    /// Emit an item to stdout/file system depending on the current configuration
529    #[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    // Find executable targets whose `path` is `<virtual>`, and strip the path out - this
606    // is required due to a bug in the project manifest parser that requires executable
607    // targets to have a `path`, but virtual targets don't have paths
608    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        // We have to rewrite this package without the path
614        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    // If the namespace is the same as the package name, then the default
665    // namespace is being used, and we should rewrite it to use the correct
666    // namespace, derived from the component id
667    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) {}