midenc_session/
outputs.rs

1use alloc::{
2    borrow::ToOwned, boxed::Box, collections::BTreeMap, fmt, format, str::FromStr, string::String,
3};
4use std::{
5    ffi::OsStr,
6    path::{Path, PathBuf},
7};
8
9/// The type of output to produce for a given [OutputType], when multiple options are available
10#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub enum OutputMode {
12    /// Pretty-print the textual form of the current [OutputType]
13    Text,
14    /// Encode the current [OutputType] in its canonical binary format
15    Binary,
16}
17
18/// This enum represents the type of outputs the compiler can produce
19#[derive(Debug, Copy, Clone, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
20#[cfg_attr(feature = "std", derive(clap::ValueEnum))]
21pub enum OutputType {
22    /// The compiler will emit the parse tree of the input, if applicable
23    Ast,
24    /// The compiler will emit Miden IR
25    Hir,
26    /// The compiler will emit Miden Assembly text
27    Masm,
28    /// The compiler will emit a Merkalized Abstract Syntax Tree in text form
29    Mast,
30    /// The compiler will emit a MAST library in binary form
31    Masl,
32    /// The compiler will emit a MAST package in binary form
33    #[default]
34    Masp,
35}
36impl OutputType {
37    /// Returns true if this output type is an intermediate artifact produced during compilation
38    pub fn is_intermediate(&self) -> bool {
39        !matches!(self, Self::Mast | Self::Masl | Self::Masp)
40    }
41
42    pub fn extension(&self) -> &'static str {
43        match self {
44            Self::Ast => "ast",
45            Self::Hir => "hir",
46            Self::Masm => "masm",
47            Self::Mast => "mast",
48            Self::Masl => "masl",
49            Self::Masp => "masp",
50        }
51    }
52
53    pub fn shorthand_display() -> String {
54        format!(
55            "`{}`, `{}`, `{}`, `{}`, `{}`, `{}`",
56            Self::Ast,
57            Self::Hir,
58            Self::Masm,
59            Self::Mast,
60            Self::Masl,
61            Self::Masp,
62        )
63    }
64
65    pub fn all() -> [OutputType; 6] {
66        [
67            OutputType::Ast,
68            OutputType::Hir,
69            OutputType::Masm,
70            OutputType::Mast,
71            OutputType::Masl,
72            OutputType::Masp,
73        ]
74    }
75}
76impl fmt::Display for OutputType {
77    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
78        match self {
79            Self::Ast => f.write_str("ast"),
80            Self::Hir => f.write_str("hir"),
81            Self::Masm => f.write_str("masm"),
82            Self::Mast => f.write_str("mast"),
83            Self::Masl => f.write_str("masl"),
84            Self::Masp => f.write_str("masp"),
85        }
86    }
87}
88impl FromStr for OutputType {
89    type Err = ();
90
91    fn from_str(s: &str) -> Result<Self, Self::Err> {
92        match s {
93            "ast" => Ok(Self::Ast),
94            "hir" => Ok(Self::Hir),
95            "masm" => Ok(Self::Masm),
96            "mast" => Ok(Self::Mast),
97            "masl" => Ok(Self::Masl),
98            "masp" => Ok(Self::Masp),
99            _ => Err(()),
100        }
101    }
102}
103
104#[derive(Debug, Clone)]
105pub enum OutputFile {
106    Real(PathBuf),
107    Stdout,
108}
109impl OutputFile {
110    pub fn parent(&self) -> Option<&Path> {
111        match self {
112            Self::Real(ref path) => path.parent(),
113            Self::Stdout => None,
114        }
115    }
116
117    pub fn filestem(&self) -> Option<&OsStr> {
118        match self {
119            Self::Real(ref path) => path.file_stem(),
120            Self::Stdout => None,
121        }
122    }
123
124    pub fn is_stdout(&self) -> bool {
125        matches!(self, Self::Stdout)
126    }
127
128    #[cfg(feature = "std")]
129    pub fn is_tty(&self) -> bool {
130        use std::io::IsTerminal;
131        match self {
132            Self::Real(_) => false,
133            Self::Stdout => std::io::stdout().is_terminal(),
134        }
135    }
136
137    #[cfg(not(feature = "std"))]
138    pub fn is_tty(&self) -> bool {
139        false
140    }
141
142    pub fn as_path(&self) -> Option<&Path> {
143        match self {
144            Self::Real(ref path) => Some(path.as_ref()),
145            Self::Stdout => None,
146        }
147    }
148
149    pub fn file_for_writing(
150        &self,
151        outputs: &OutputFiles,
152        ty: OutputType,
153        name: Option<&str>,
154    ) -> PathBuf {
155        match self {
156            Self::Real(ref path) => path.clone(),
157            Self::Stdout => outputs.temp_path(ty, name),
158        }
159    }
160}
161impl fmt::Display for OutputFile {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            Self::Real(ref path) => write!(f, "{}", path.display()),
165            Self::Stdout => write!(f, "stdout"),
166        }
167    }
168}
169
170#[derive(Debug)]
171pub struct OutputFiles {
172    stem: String,
173    /// The compiler working directory
174    pub cwd: PathBuf,
175    /// The directory in which to place temporaries or intermediate artifacts
176    pub tmp_dir: PathBuf,
177    /// The directory in which to place objects produced by the current compiler operation
178    ///
179    /// This directory is intended for non-intermediate artifacts, though it may be used
180    /// to derive `tmp_dir` elsewhere. You should prefer to use `tmp_dir` for files which
181    /// are internal details of the compiler.
182    pub out_dir: PathBuf,
183    /// If specified, the specific path at which to write the compiler output.
184    ///
185    /// This _only_ applies to the final output, i.e. the `.masl` library or executable.
186    pub out_file: Option<OutputFile>,
187    /// The raw output types requested by the user on the command line
188    pub outputs: OutputTypes,
189}
190impl OutputFiles {
191    pub fn new(
192        stem: String,
193        cwd: PathBuf,
194        out_dir: PathBuf,
195        out_file: Option<OutputFile>,
196        tmp_dir: PathBuf,
197        outputs: OutputTypes,
198    ) -> Self {
199        Self {
200            stem,
201            cwd,
202            tmp_dir,
203            out_dir,
204            out_file,
205            outputs,
206        }
207    }
208
209    /// Return the [OutputFile] representing where an output of `ty` type should be written,
210    /// with an optional `name`, which overrides the file stem of the resulting path.
211    pub fn output_file(&self, ty: OutputType, name: Option<&str>) -> OutputFile {
212        let default_name = name.unwrap_or(self.stem.as_str());
213        self.outputs
214            .get(&ty)
215            .and_then(|p| p.to_owned())
216            .map(|of| match of {
217                OutputFile::Real(path) => OutputFile::Real({
218                    let path = if path.is_absolute() {
219                        path
220                    } else {
221                        self.cwd.join(path)
222                    };
223                    if path.is_dir() {
224                        path.join(default_name).with_extension(ty.extension())
225                    } else if let Some(name) = name {
226                        path.with_stem_and_extension(name, ty.extension())
227                    } else {
228                        path
229                    }
230                }),
231                out @ OutputFile::Stdout => out,
232            })
233            .unwrap_or_else(|| {
234                let out = if ty.is_intermediate() {
235                    self.with_directory_and_extension(&self.tmp_dir, ty.extension())
236                } else if let Some(output_file) = self.out_file.as_ref() {
237                    return output_file.clone();
238                } else {
239                    self.with_directory_and_extension(&self.out_dir, ty.extension())
240                };
241                OutputFile::Real(if let Some(name) = name {
242                    out.with_stem(name)
243                } else {
244                    out
245                })
246            })
247    }
248
249    /// Return the most appropriate file path for an output of `ty` type.
250    ///
251    /// The returned path _may_ be precise, if a specific file path was chosen by the user for
252    /// the given output type, but in general the returned path will be derived from the current
253    /// `self.stem`, and is thus an appropriate default path for the given output.
254    pub fn output_path(&self, ty: OutputType) -> PathBuf {
255        match self.output_file(ty, None) {
256            OutputFile::Real(path) => path,
257            OutputFile::Stdout => {
258                if ty.is_intermediate() {
259                    self.with_directory_and_extension(&self.tmp_dir, ty.extension())
260                } else if let Some(output_file) = self.out_file.as_ref().and_then(|of| of.as_path())
261                {
262                    output_file.to_path_buf()
263                } else {
264                    self.with_directory_and_extension(&self.out_dir, ty.extension())
265                }
266            }
267        }
268    }
269
270    /// Constructs a file path for a temporary file of the given output type, with an optional name,
271    /// falling back to `self.stem` if no name is provided.
272    ///
273    /// The file path is always a child of `self.tmp_dir`
274    pub fn temp_path(&self, ty: OutputType, name: Option<&str>) -> PathBuf {
275        self.tmp_dir
276            .join(name.unwrap_or(self.stem.as_str()))
277            .with_extension(ty.extension())
278    }
279
280    /// Build a file path which is either:
281    ///
282    /// * If `self.out_file` is set to a real path, returns it with extension set to `extension`
283    /// * Otherwise, calls [with_directory_and_extension] with `self.out_dir` and `extension`
284    pub fn with_extension(&self, extension: &str) -> PathBuf {
285        match self.out_file.as_ref() {
286            Some(OutputFile::Real(ref path)) => path.with_extension(extension),
287            Some(OutputFile::Stdout) | None => {
288                self.with_directory_and_extension(&self.out_dir, extension)
289            }
290        }
291    }
292
293    /// Build a file path whose parent is `directory`, file stem is `self.stem`, and extension is
294    /// `extension`
295    #[inline]
296    fn with_directory_and_extension(&self, directory: &Path, extension: &str) -> PathBuf {
297        directory.join(&self.stem).with_extension(extension)
298    }
299}
300
301#[derive(Debug, Clone, Default)]
302pub struct OutputTypes(BTreeMap<OutputType, Option<OutputFile>>);
303impl OutputTypes {
304    pub fn new<I: IntoIterator<Item = OutputTypeSpec>>(entries: I) -> Result<Self, clap::Error> {
305        let entries = entries.into_iter();
306        let mut map = BTreeMap::default();
307        for spec in entries {
308            match spec {
309                OutputTypeSpec::All { path } => {
310                    if !map.is_empty() {
311                        return Err(clap::Error::raw(
312                            clap::error::ErrorKind::ValueValidation,
313                            "--emit=all cannot be combined with other --emit types",
314                        ));
315                    }
316                    if let Some(OutputFile::Real(ref path)) = &path {
317                        if path.extension().is_some() {
318                            return Err(clap::Error::raw(
319                                clap::error::ErrorKind::ValueValidation,
320                                "invalid path for --emit=all: must be a directory",
321                            ));
322                        }
323                    }
324                    for ty in OutputType::all() {
325                        map.insert(ty, path.clone());
326                    }
327                }
328                OutputTypeSpec::Typed { output_type, path } => {
329                    if path.is_some() {
330                        if matches!(map.get(&output_type), Some(Some(OutputFile::Real(_)))) {
331                            return Err(clap::Error::raw(
332                                clap::error::ErrorKind::ValueValidation,
333                                format!(
334                                    "conflicting --emit options given for output type \
335                                     '{output_type}'"
336                                ),
337                            ));
338                        }
339                    } else if matches!(map.get(&output_type), Some(Some(_))) {
340                        continue;
341                    }
342                    map.insert(output_type, path);
343                }
344            }
345        }
346        Ok(Self(map))
347    }
348
349    pub fn get(&self, key: &OutputType) -> Option<&Option<OutputFile>> {
350        self.0.get(key)
351    }
352
353    pub fn insert(&mut self, key: OutputType, value: Option<OutputFile>) {
354        self.0.insert(key, value);
355    }
356
357    pub fn clear(&mut self) {
358        self.0.clear();
359    }
360
361    pub fn contains_key(&self, key: &OutputType) -> bool {
362        self.0.contains_key(key)
363    }
364
365    pub fn iter(&self) -> std::collections::btree_map::Iter<'_, OutputType, Option<OutputFile>> {
366        self.0.iter()
367    }
368
369    pub fn keys(&self) -> std::collections::btree_map::Keys<'_, OutputType, Option<OutputFile>> {
370        self.0.keys()
371    }
372
373    pub fn values(
374        &self,
375    ) -> std::collections::btree_map::Values<'_, OutputType, Option<OutputFile>> {
376        self.0.values()
377    }
378
379    #[inline(always)]
380    pub fn is_empty(&self) -> bool {
381        self.0.is_empty()
382    }
383
384    pub fn len(&self) -> usize {
385        self.0.len()
386    }
387
388    pub fn should_link(&self) -> bool {
389        self.0.keys().any(|k| {
390            matches!(
391                k,
392                OutputType::Hir
393                    | OutputType::Masm
394                    | OutputType::Mast
395                    | OutputType::Masl
396                    | OutputType::Masp
397            )
398        })
399    }
400
401    pub fn should_codegen(&self) -> bool {
402        self.0.keys().any(|k| {
403            matches!(k, OutputType::Masm | OutputType::Mast | OutputType::Masl | OutputType::Masp)
404        })
405    }
406
407    pub fn should_assemble(&self) -> bool {
408        self.0
409            .keys()
410            .any(|k| matches!(k, OutputType::Mast | OutputType::Masl | OutputType::Masp))
411    }
412}
413
414/// This type describes an output type with optional path specification
415#[derive(Debug, Clone)]
416pub enum OutputTypeSpec {
417    All {
418        path: Option<OutputFile>,
419    },
420    Typed {
421        output_type: OutputType,
422        path: Option<OutputFile>,
423    },
424}
425impl clap::builder::ValueParserFactory for OutputTypeSpec {
426    type Parser = OutputTypeParser;
427
428    fn value_parser() -> Self::Parser {
429        OutputTypeParser
430    }
431}
432
433#[doc(hidden)]
434#[derive(Clone)]
435pub struct OutputTypeParser;
436impl clap::builder::TypedValueParser for OutputTypeParser {
437    type Value = OutputTypeSpec;
438
439    fn possible_values(
440        &self,
441    ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
442        use clap::builder::PossibleValue;
443        Some(Box::new(
444            [
445                PossibleValue::new("ast").help("Abstract Syntax Tree (text)"),
446                PossibleValue::new("hir").help("High-level Intermediate Representation (text)"),
447                PossibleValue::new("masm").help("Miden Assembly (text)"),
448                PossibleValue::new("mast").help("Merkelized Abstract Syntax Tree (text)"),
449                PossibleValue::new("masl").help("Merkelized Abstract Syntax Tree (binary)"),
450                PossibleValue::new("masp").help("Miden Assembly Package Format (binary)"),
451                PossibleValue::new("all").help("All of the above"),
452            ]
453            .into_iter(),
454        ))
455    }
456
457    fn parse_ref(
458        &self,
459        _cmd: &clap::Command,
460        _arg: Option<&clap::Arg>,
461        value: &OsStr,
462    ) -> Result<Self::Value, clap::error::Error> {
463        use clap::error::{Error, ErrorKind};
464
465        let output_type = value.to_str().ok_or_else(|| Error::new(ErrorKind::InvalidUtf8))?;
466
467        let (shorthand, path) = match output_type.split_once('=') {
468            None => (output_type, None),
469            Some((shorthand, "-")) => (shorthand, Some(OutputFile::Stdout)),
470            Some((shorthand, path)) => (shorthand, Some(OutputFile::Real(PathBuf::from(path)))),
471        };
472        if shorthand == "all" {
473            return Ok(OutputTypeSpec::All { path });
474        }
475        let output_type = shorthand.parse::<OutputType>().map_err(|_| {
476            Error::raw(
477                ErrorKind::InvalidValue,
478                format!(
479                    "invalid output type: `{shorthand}` - expected one of: {display}",
480                    display = OutputType::shorthand_display()
481                ),
482            )
483        })?;
484        Ok(OutputTypeSpec::Typed { output_type, path })
485    }
486}
487
488trait PathMut {
489    fn with_stem(self, stem: impl AsRef<OsStr>) -> PathBuf;
490    fn with_stem_and_extension(self, stem: impl AsRef<OsStr>, ext: impl AsRef<OsStr>) -> PathBuf;
491}
492impl PathMut for &Path {
493    fn with_stem(self, stem: impl AsRef<OsStr>) -> PathBuf {
494        let mut path = self.with_file_name(stem);
495        if let Some(ext) = self.extension() {
496            path.set_extension(ext);
497        }
498        path
499    }
500
501    fn with_stem_and_extension(self, stem: impl AsRef<OsStr>, ext: impl AsRef<OsStr>) -> PathBuf {
502        let mut path = self.with_file_name(stem);
503        path.set_extension(ext);
504        path
505    }
506}
507impl PathMut for PathBuf {
508    fn with_stem(mut self, stem: impl AsRef<OsStr>) -> PathBuf {
509        if let Some(ext) = self.extension() {
510            let ext = ext.to_string_lossy().into_owned();
511            self.with_stem_and_extension(stem, ext)
512        } else {
513            self.set_file_name(stem);
514            self
515        }
516    }
517
518    fn with_stem_and_extension(
519        mut self,
520        stem: impl AsRef<OsStr>,
521        ext: impl AsRef<OsStr>,
522    ) -> PathBuf {
523        self.set_file_name(stem);
524        self.set_extension(ext);
525        self
526    }
527}