midenc_session/
outputs.rs

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