midenc_session/
lib.rs

1#![no_std]
2#![feature(debug_closure_helpers)]
3#![feature(specialization)]
4#![feature(slice_split_once)]
5// Specialization
6#![allow(incomplete_features)]
7#![deny(warnings)]
8
9extern crate alloc;
10#[cfg(feature = "std")]
11extern crate std;
12
13use alloc::{
14    borrow::ToOwned,
15    format,
16    string::{String, ToString},
17    vec::Vec,
18};
19use core::str::FromStr;
20
21mod color;
22pub mod diagnostics;
23#[cfg(feature = "std")]
24mod duration;
25mod emit;
26mod emitter;
27pub mod flags;
28mod inputs;
29mod libs;
30mod options;
31mod outputs;
32mod path;
33#[cfg(feature = "std")]
34mod statistics;
35
36use alloc::{fmt, sync::Arc};
37
38/// The version associated with the current compiler toolchain
39pub const MIDENC_BUILD_VERSION: &str = env!("MIDENC_BUILD_VERSION");
40
41/// The git revision associated with the current compiler toolchain
42pub const MIDENC_BUILD_REV: &str = env!("MIDENC_BUILD_REV");
43
44pub use miden_assembly;
45use midenc_hir_symbol::Symbol;
46
47pub use self::{
48    color::ColorChoice,
49    diagnostics::{DiagnosticsHandler, Emitter, SourceManager},
50    emit::{Emit, Writer},
51    flags::{ArgMatches, CompileFlag, CompileFlags, FlagAction},
52    inputs::{FileName, FileType, InputFile, InputType, InvalidInputError},
53    libs::{
54        add_target_link_libraries, LibraryKind, LibraryNamespace, LibraryPath,
55        LibraryPathComponent, LinkLibrary, STDLIB,
56    },
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/// The type of project being compiled
65#[derive(Debug, Copy, Clone, Default)]
66pub enum ProjectType {
67    /// Compile a Miden program that can be run on the Miden VM
68    #[default]
69    Program,
70    /// Compile a Miden library which can be linked into a program
71    Library,
72}
73impl ProjectType {
74    pub fn default_for_target(target: TargetEnv) -> Self {
75        match target {
76            // We default to compiling a program unless we find later
77            // that we do not have an entrypoint.
78            TargetEnv::Base | TargetEnv::Rollup { .. } => Self::Program,
79            // The emulator can run either programs or individual library functions,
80            // so we compile as a library and delegate the choice of how to run it
81            // to the emulator
82            TargetEnv::Emu => Self::Library,
83        }
84    }
85}
86
87/// This struct provides access to all of the metadata and configuration
88/// needed during a single compilation session.
89pub struct Session {
90    /// The name of this session
91    pub name: String,
92    /// Configuration for the current compiler session
93    pub options: Options,
94    /// The current source manager
95    pub source_manager: Arc<dyn SourceManager>,
96    /// The current diagnostics handler
97    pub diagnostics: Arc<DiagnosticsHandler>,
98    /// The inputs being compiled
99    pub inputs: Vec<InputFile>,
100    /// The outputs to be produced by the compiler during compilation
101    pub output_files: OutputFiles,
102    /// Statistics gathered from the current compiler session
103    #[cfg(feature = "std")]
104    pub statistics: Statistics,
105}
106
107impl fmt::Debug for Session {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        let inputs = self.inputs.iter().map(|input| input.file_name()).collect::<Vec<_>>();
110        f.debug_struct("Session")
111            .field("name", &self.name)
112            .field("options", &self.options)
113            .field("inputs", &inputs)
114            .field("output_files", &self.output_files)
115            .finish_non_exhaustive()
116    }
117}
118
119impl Session {
120    pub fn new<I>(
121        inputs: I,
122        output_dir: Option<PathBuf>,
123        output_file: Option<OutputFile>,
124        target_dir: PathBuf,
125        options: Options,
126        emitter: Option<Arc<dyn Emitter>>,
127        source_manager: Arc<dyn SourceManager>,
128    ) -> Self
129    where
130        I: IntoIterator<Item = InputFile>,
131    {
132        let inputs = inputs.into_iter().collect::<Vec<_>>();
133
134        Self::make(inputs, output_dir, output_file, target_dir, options, emitter, source_manager)
135    }
136
137    fn make(
138        inputs: Vec<InputFile>,
139        output_dir: Option<PathBuf>,
140        output_file: Option<OutputFile>,
141        target_dir: PathBuf,
142        options: Options,
143        emitter: Option<Arc<dyn Emitter>>,
144        source_manager: Arc<dyn SourceManager>,
145    ) -> Self {
146        log::debug!(target: "driver", "creating session for {} inputs:", inputs.len());
147        if log::log_enabled!(target: "driver", log::Level::Debug) {
148            for input in inputs.iter() {
149                log::debug!(target: "driver", " - {} ({})", input.file_name(), input.file_type());
150            }
151            log::debug!(
152                target: "driver",
153                " | outputs_dir = {}",
154                output_dir
155                    .as_ref()
156                    .map(|p| p.display().to_string())
157                    .unwrap_or("<unset>".to_string())
158            );
159            log::debug!(
160                target: "driver",
161                " | output_file = {}",
162                output_file.as_ref().map(|of| of.to_string()).unwrap_or("<unset>".to_string())
163            );
164            log::debug!(target: "driver", " | target_dir = {}", target_dir.display());
165        }
166        let diagnostics = Arc::new(DiagnosticsHandler::new(
167            options.diagnostics,
168            source_manager.clone(),
169            emitter.unwrap_or_else(|| options.default_emitter()),
170        ));
171
172        let output_dir = output_dir
173            .as_deref()
174            .or_else(|| output_file.as_ref().and_then(|of| of.parent()))
175            .map(|path| path.to_path_buf());
176
177        if let Some(output_dir) = output_dir.as_deref() {
178            log::debug!(target: "driver", " | output dir = {}", output_dir.display());
179        } else {
180            log::debug!(target: "driver", " | output dir = <unset>");
181        }
182
183        log::debug!(target: "driver", " | target = {}", &options.target);
184        log::debug!(target: "driver", " | type = {:?}", &options.project_type);
185        if log::log_enabled!(target: "driver", log::Level::Debug) {
186            for lib in options.link_libraries.iter() {
187                if let Some(path) = lib.path.as_deref() {
188                    log::debug!(target: "driver", " | linking {} library '{}' from {}", &lib.kind, &lib.name, path.display());
189                } else {
190                    log::debug!(target: "driver", " | linking {} library '{}'", &lib.kind, &lib.name);
191                }
192            }
193        }
194
195        let name = options
196            .name
197            .clone()
198            .or_else(|| {
199                log::debug!(target: "driver", "no name specified, attempting to derive from output file");
200                output_file.as_ref().and_then(|of| of.filestem().map(|stem| stem.to_string()))
201            })
202            .unwrap_or_else(|| {
203                log::debug!(target: "driver", "unable to derive name from output file, deriving from input");
204                match inputs.first() {
205                    Some(InputFile {
206                        file: InputType::Real(ref path),
207                        ..
208                    }) => path
209                        .file_stem()
210                        .and_then(|stem| stem.to_str())
211                        .or_else(|| path.extension().and_then(|stem| stem.to_str()))
212                        .unwrap_or_else(|| {
213                            panic!(
214                                "invalid input path: '{}' has no file stem or extension",
215                                path.display()
216                            )
217                        })
218                        .to_string(),
219                    Some(
220                        input @ InputFile {
221                            file: InputType::Stdin { ref name, .. },
222                            ..
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                    None => "out".to_owned(),
239                }
240            });
241        log::debug!(target: "driver", "artifact name set to '{name}'");
242
243        let output_files = OutputFiles::new(
244            name.clone(),
245            options.current_dir.clone(),
246            output_dir.unwrap_or_else(|| options.current_dir.clone()),
247            output_file,
248            target_dir,
249            options.output_types.clone(),
250        );
251
252        Self {
253            name,
254            options,
255            source_manager,
256            diagnostics,
257            inputs,
258            output_files,
259            #[cfg(feature = "std")]
260            statistics: Default::default(),
261        }
262    }
263
264    pub fn with_project_type(mut self, ty: ProjectType) -> Self {
265        self.options.project_type = ty;
266        self
267    }
268
269    #[doc(hidden)]
270    pub fn with_output_type(mut self, ty: OutputType, path: Option<OutputFile>) -> Self {
271        self.output_files.outputs.insert(ty, path.clone());
272        self.options.output_types.insert(ty, path.clone());
273        self
274    }
275
276    #[doc(hidden)]
277    pub fn with_extra_flags(mut self, flags: CompileFlags) -> Self {
278        self.options.set_extra_flags(flags);
279        self
280    }
281
282    /// Get the value of a custom flag with action `FlagAction::SetTrue` or `FlagAction::SetFalse`
283    #[inline]
284    pub fn get_flag(&self, name: &str) -> bool {
285        self.options.flags.get_flag(name)
286    }
287
288    /// Get the count of a specific custom flag with action `FlagAction::Count`
289    #[inline]
290    pub fn get_flag_count(&self, name: &str) -> usize {
291        self.options.flags.get_flag_count(name)
292    }
293
294    /// Get the remaining [ArgMatches] left after parsing the base session configuration
295    #[inline]
296    pub fn matches(&self) -> &ArgMatches {
297        self.options.flags.matches()
298    }
299
300    /// The name of this session (used as the name of the project, output file, etc.)
301    pub fn name(&self) -> &str {
302        &self.name
303    }
304
305    /// Get the [OutputFile] to write the assembled MAST output to
306    pub fn out_file(&self) -> OutputFile {
307        let out_file = self.output_files.output_file(OutputType::Masl, None);
308
309        if let OutputFile::Real(ref path) = out_file {
310            self.check_file_is_writeable(path);
311        }
312
313        out_file
314    }
315
316    #[cfg(not(feature = "std"))]
317    fn check_file_is_writeable(&self, file: &Path) {
318        panic!(
319            "Compiler exited with a fatal error: cannot write '{}' - compiler was built without \
320             standard library",
321            file.display()
322        );
323    }
324
325    #[cfg(feature = "std")]
326    fn check_file_is_writeable(&self, file: &Path) {
327        if let Ok(m) = file.metadata() {
328            if m.permissions().readonly() {
329                panic!(
330                    "Compiler exited with a fatal error: file is not writeable: {}",
331                    file.display()
332                );
333            }
334        }
335    }
336
337    /// Returns true if the compiler should exit after parsing the input
338    pub fn parse_only(&self) -> bool {
339        self.options.parse_only
340    }
341
342    /// Returns true if the compiler should exit after performing semantic analysis
343    pub fn analyze_only(&self) -> bool {
344        self.options.analyze_only
345    }
346
347    /// Returns true if the compiler should exit after applying rewrites to the IR
348    pub fn rewrite_only(&self) -> bool {
349        let link_or_masm_requested = self.should_link() || self.should_codegen();
350        !self.options.parse_only && !self.options.analyze_only && !link_or_masm_requested
351    }
352
353    /// Returns true if an [OutputType] that requires linking + assembly was requested
354    pub fn should_link(&self) -> bool {
355        self.options.output_types.should_link() && !self.options.no_link
356    }
357
358    /// Returns true if an [OutputType] that requires generating Miden Assembly was requested
359    pub fn should_codegen(&self) -> bool {
360        self.options.output_types.should_codegen() && !self.options.link_only
361    }
362
363    /// Returns true if an [OutputType] that requires assembling MAST was requested
364    pub fn should_assemble(&self) -> bool {
365        self.options.output_types.should_assemble() && !self.options.link_only
366    }
367
368    /// Returns true if the given [OutputType] should be emitted as an output
369    pub fn should_emit(&self, ty: OutputType) -> bool {
370        self.options.output_types.contains_key(&ty)
371    }
372
373    /// Returns true if IR should be printed to stdout, after executing a pass named `pass`
374    pub fn should_print_ir(&self, pass: &str) -> bool {
375        self.options.print_ir_after_all
376            || self.options.print_ir_after_pass.iter().any(|p| p == pass)
377    }
378
379    /// Returns true if CFG should be printed to stdout, after executing a pass named `pass`
380    pub fn should_print_cfg(&self, pass: &str) -> bool {
381        self.options.print_cfg_after_all
382            || self.options.print_cfg_after_pass.iter().any(|p| p == pass)
383    }
384
385    /// Print the given emittable IR to stdout, as produced by a pass with name `pass`
386    #[cfg(feature = "std")]
387    pub fn print(&self, ir: impl Emit, pass: &str) -> anyhow::Result<()> {
388        if self.should_print_ir(pass) {
389            ir.write_to_stdout(self)?;
390        }
391        Ok(())
392    }
393
394    /// Get the path to emit the given [OutputType] to
395    pub fn emit_to(&self, ty: OutputType, name: Option<Symbol>) -> Option<PathBuf> {
396        if self.should_emit(ty) {
397            match self.output_files.output_file(ty, name.map(|n| n.as_str())) {
398                OutputFile::Real(path) => Some(path),
399                OutputFile::Stdout => None,
400            }
401        } else {
402            None
403        }
404    }
405
406    /// Emit an item to stdout/file system depending on the current configuration
407    #[cfg(feature = "std")]
408    pub fn emit<E: Emit>(&self, mode: OutputMode, item: &E) -> anyhow::Result<()> {
409        let output_type = item.output_type(mode);
410        if self.should_emit(output_type) {
411            let name = item.name().map(|n| n.as_str());
412            match self.output_files.output_file(output_type, name) {
413                OutputFile::Real(path) => {
414                    item.write_to_file(&path, mode, self)?;
415                }
416                OutputFile::Stdout => {
417                    let stdout = std::io::stdout().lock();
418                    item.write_to(stdout, mode, self)?;
419                }
420            }
421        }
422
423        Ok(())
424    }
425
426    #[cfg(not(feature = "std"))]
427    pub fn emit<E: Emit>(&self, _mode: OutputMode, _item: &E) -> anyhow::Result<()> {
428        Ok(())
429    }
430}
431
432/// This enum describes the different target environments targetable by the compiler
433#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
434pub enum TargetEnv {
435    /// The emulator environment, which has a more restrictive instruction set
436    Emu,
437    /// The default Miden VM environment
438    #[default]
439    Base,
440    /// The Miden Rollup environment, using the Rollup kernel
441    Rollup { target: RollupTarget },
442}
443impl fmt::Display for TargetEnv {
444    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
445        match self {
446            Self::Emu => f.write_str("emu"),
447            Self::Base => f.write_str("base"),
448            Self::Rollup { target } => f.write_str(&format!("rollup:{}", target)),
449        }
450    }
451}
452
453impl FromStr for TargetEnv {
454    type Err = anyhow::Error;
455
456    fn from_str(s: &str) -> Result<Self, Self::Err> {
457        match s {
458            "emu" => Ok(Self::Emu),
459            "base" => Ok(Self::Base),
460            "rollup" => Ok(Self::Rollup {
461                target: RollupTarget::default(),
462            }),
463            "rollup:account" => Ok(Self::Rollup {
464                target: RollupTarget::Account,
465            }),
466            "rollup:note_script" => Ok(Self::Rollup {
467                target: RollupTarget::NoteScript,
468            }),
469            _ => Err(anyhow::anyhow!("invalid target environment: {}", s)),
470        }
471    }
472}
473
474/// This enum describes the different rollup targets
475#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
476pub enum RollupTarget {
477    #[default]
478    Account,
479    NoteScript,
480}
481
482impl fmt::Display for RollupTarget {
483    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
484        match self {
485            Self::Account => f.write_str("account"),
486            Self::NoteScript => f.write_str("note_script"),
487        }
488    }
489}