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