Skip to main content

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 smallvec::SmallVec;
10
11use crate::{Path, PathBuf};
12
13/// Escape `name` for use as a single filesystem path component (e.g. a file stem).
14///
15/// This is used when emitting artifacts whose names may contain characters that are legal in
16/// compiler/session identifiers, but are problematic (or even invalid) as filenames on common
17/// filesystems.
18fn escape_path_component(name: &str) -> Cow<'_, str> {
19    if name.is_empty() {
20        return Cow::Borrowed("_");
21    }
22
23    let is_safe = name != "."
24        && name != ".."
25        && name.chars().all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-'));
26    if is_safe {
27        return Cow::Borrowed(name);
28    }
29
30    let mut escaped = String::with_capacity(name.len());
31    for ch in name.chars() {
32        if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-') {
33            escaped.push(ch);
34        } else {
35            escaped.push('_');
36        }
37    }
38
39    match escaped.as_str() {
40        "" | "." | ".." => Cow::Borrowed("_"),
41        _ => Cow::Owned(escaped),
42    }
43}
44
45/// The type of output to produce for a given [OutputType], when multiple options are available
46#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
47pub enum OutputMode {
48    /// Pretty-print the textual form of the current [OutputType]
49    Text,
50    /// Encode the current [OutputType] in its canonical binary format
51    Binary,
52}
53
54/// This enum represents the type of outputs the compiler can produce
55#[derive(Debug, Copy, Clone, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
56#[cfg_attr(feature = "std", derive(clap::ValueEnum))]
57pub enum OutputType {
58    /// The compiler will emit the parse tree of the input, if applicable
59    Ast,
60    /// The compiler will emit WebAssembly text format (WAT), if applicable
61    Wat,
62    /// The compiler will emit Miden IR
63    Hir,
64    /// The compiler will emit Miden Assembly text
65    Masm,
66    /// The compiler will emit a Merkalized Abstract Syntax Tree in text form
67    Mast,
68    /// The compiler will emit a MAST package in binary form
69    #[default]
70    Masp,
71}
72impl OutputType {
73    /// Returns true if this output type is an intermediate artifact produced during compilation
74    pub fn is_intermediate(&self) -> bool {
75        !matches!(self, Self::Mast | Self::Masp)
76    }
77
78    pub fn extension(&self) -> &'static str {
79        match self {
80            Self::Ast => "ast",
81            Self::Wat => "wat",
82            Self::Hir => "hir",
83            Self::Masm => "masm",
84            Self::Mast => "mast",
85            Self::Masp => "masp",
86        }
87    }
88
89    pub fn shorthand_display() -> String {
90        format!(
91            "`{}`, `{}`, `{}`, `{}`, `{}`, `{}`",
92            Self::Ast,
93            Self::Wat,
94            Self::Hir,
95            Self::Masm,
96            Self::Mast,
97            Self::Masp,
98        )
99    }
100
101    pub const fn all() -> &'static [OutputType] {
102        &[
103            OutputType::Ast,
104            OutputType::Wat,
105            OutputType::Hir,
106            OutputType::Masm,
107            OutputType::Mast,
108            OutputType::Masp,
109        ]
110    }
111
112    /// Returns the subset of [OutputType] values considered "intermediate" for convenience
113    /// emission (WAT, HIR, MASM).
114    pub const fn ir() -> &'static [OutputType] {
115        &[OutputType::Wat, OutputType::Hir, OutputType::Masm]
116    }
117}
118impl fmt::Display for OutputType {
119    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120        match self {
121            Self::Ast => f.write_str("ast"),
122            Self::Wat => f.write_str("wat"),
123            Self::Hir => f.write_str("hir"),
124            Self::Masm => f.write_str("masm"),
125            Self::Mast => f.write_str("mast"),
126            Self::Masp => f.write_str("masp"),
127        }
128    }
129}
130impl FromStr for OutputType {
131    type Err = ();
132
133    fn from_str(s: &str) -> Result<Self, Self::Err> {
134        match s {
135            "ast" => Ok(Self::Ast),
136            "wat" => Ok(Self::Wat),
137            "hir" => Ok(Self::Hir),
138            "masm" => Ok(Self::Masm),
139            "mast" => Ok(Self::Mast),
140            "masp" => Ok(Self::Masp),
141            _ => Err(()),
142        }
143    }
144}
145
146#[derive(Debug, Clone)]
147pub enum OutputFile {
148    Real(PathBuf),
149    /// A directory in which to place outputs.
150    ///
151    /// This is distinct from [OutputFile::Real] because callers may want a path to be treated as a
152    /// directory even if it does not exist yet.
153    Directory(PathBuf),
154    Stdout,
155}
156impl OutputFile {
157    pub fn parent(&self) -> Option<&Path> {
158        match self {
159            Self::Real(path) => path.parent(),
160            Self::Directory(path) => Some(path.as_ref()),
161            Self::Stdout => None,
162        }
163    }
164
165    pub fn filestem(&self) -> Option<Cow<'_, str>> {
166        match self {
167            Self::Real(path) => path.file_stem().map(|stem| stem.to_string_lossy()),
168            Self::Directory(_) => None,
169            Self::Stdout => None,
170        }
171    }
172
173    pub fn is_stdout(&self) -> bool {
174        matches!(self, Self::Stdout)
175    }
176
177    #[cfg(feature = "std")]
178    pub fn is_tty(&self) -> bool {
179        use std::io::IsTerminal;
180        match self {
181            Self::Real(_) => false,
182            Self::Directory(_) => false,
183            Self::Stdout => std::io::stdout().is_terminal(),
184        }
185    }
186
187    #[cfg(not(feature = "std"))]
188    pub fn is_tty(&self) -> bool {
189        false
190    }
191
192    pub fn as_path(&self) -> Option<&Path> {
193        match self {
194            Self::Real(path) => Some(path.as_ref()),
195            Self::Directory(path) => Some(path.as_ref()),
196            Self::Stdout => None,
197        }
198    }
199
200    pub fn file_for_writing(
201        &self,
202        outputs: &OutputFiles,
203        ty: OutputType,
204        name: Option<&str>,
205    ) -> PathBuf {
206        match self {
207            Self::Real(path) => path.clone(),
208            Self::Directory(dir) => {
209                let dir = if dir.is_absolute() {
210                    dir.clone()
211                } else {
212                    outputs.cwd.join(dir)
213                };
214                let stem = escape_path_component(name.unwrap_or(outputs.stem.as_str()));
215                dir.join(stem.as_ref()).with_extension(ty.extension())
216            }
217            Self::Stdout => outputs.temp_path(ty, name),
218        }
219    }
220}
221impl fmt::Display for OutputFile {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        match self {
224            Self::Real(path) => write!(f, "{}", path.display()),
225            Self::Directory(path) => write!(f, "{}", path.display()),
226            Self::Stdout => write!(f, "stdout"),
227        }
228    }
229}
230
231#[derive(Debug, Clone)]
232pub struct OutputFiles {
233    stem: String,
234    /// The compiler working directory
235    pub cwd: PathBuf,
236    /// The directory in which to place temporaries or intermediate artifacts
237    pub tmp_dir: PathBuf,
238    /// The directory in which to place objects produced by the current compiler operation
239    ///
240    /// This directory is intended for non-intermediate artifacts, though it may be used
241    /// to derive `tmp_dir` elsewhere. You should prefer to use `tmp_dir` for files which
242    /// are internal details of the compiler.
243    pub out_dir: PathBuf,
244    /// If specified, the specific path at which to write the compiler output.
245    ///
246    /// This _only_ applies to the final output, e.g. the `.masp` package.
247    pub out_file: Option<OutputFile>,
248    /// The raw output types requested by the user on the command line
249    pub outputs: OutputTypes,
250}
251impl OutputFiles {
252    pub fn new(
253        stem: String,
254        cwd: PathBuf,
255        out_dir: PathBuf,
256        out_file: Option<OutputFile>,
257        tmp_dir: PathBuf,
258        outputs: OutputTypes,
259    ) -> Self {
260        Self {
261            stem,
262            cwd,
263            tmp_dir,
264            out_dir,
265            out_file,
266            outputs,
267        }
268    }
269
270    /// Return the [OutputFile] representing where an output of `ty` type should be written,
271    /// with an optional `name`, which overrides the file stem of the resulting path, if a
272    /// specific path was not provided.
273    pub fn output_file(&self, ty: OutputType, name: Option<&str>) -> OutputFile {
274        let requested = self.outputs.contains_key(&ty);
275        let default_name = escape_path_component(name.unwrap_or(self.stem.as_str()));
276        match self.outputs.get(&ty).and_then(|p| p.to_owned()) {
277            Some(OutputFile::Real(path)) => OutputFile::Real({
278                let path = if path.is_absolute() {
279                    path
280                } else {
281                    self.cwd.join(path)
282                };
283                if path.is_dir() {
284                    path.join(default_name.as_ref()).with_extension(ty.extension())
285                } else {
286                    path
287                }
288            }),
289            Some(OutputFile::Directory(dir)) => OutputFile::Real({
290                let dir = if dir.is_absolute() {
291                    dir
292                } else {
293                    self.cwd.join(dir)
294                };
295                dir.join(default_name.as_ref()).with_extension(ty.extension())
296            }),
297            Some(OutputFile::Stdout) => OutputFile::Stdout,
298            None => {
299                // If the user requested an output type without specifying a destination, default to
300                // the session output directory (i.e. the working directory by default). Only
301                // compiler-internal temporaries use `tmp_dir`.
302                let out = if ty.is_intermediate() {
303                    if requested {
304                        self.with_directory_and_extension(&self.out_dir, ty.extension())
305                    } else {
306                        self.with_directory_and_extension(&self.tmp_dir, ty.extension())
307                    }
308                } else if let Some(output_file) = self.out_file.as_ref() {
309                    return output_file.clone();
310                } else {
311                    self.with_directory_and_extension(&self.out_dir, ty.extension())
312                };
313                OutputFile::Real(if let Some(name) = name {
314                    let name = escape_path_component(name);
315                    out.with_stem(name.as_ref())
316                } else {
317                    out
318                })
319            }
320        }
321    }
322
323    /// Return the most appropriate file path for an output of `ty` type.
324    ///
325    /// The returned path _may_ be precise, if a specific file path was chosen by the user for
326    /// the given output type, but in general the returned path will be derived from the current
327    /// `self.stem`, and is thus an appropriate default path for the given output.
328    pub fn output_path(&self, ty: OutputType) -> PathBuf {
329        match self.output_file(ty, None) {
330            OutputFile::Real(path) => path,
331            OutputFile::Directory(_) => {
332                unreachable!("OutputFiles::output_file never returns OutputFile::Directory")
333            }
334            OutputFile::Stdout => {
335                if ty.is_intermediate() {
336                    self.with_directory_and_extension(&self.tmp_dir, ty.extension())
337                } else if let Some(output_file) = self.out_file.as_ref().and_then(|of| of.as_path())
338                {
339                    output_file.to_path_buf()
340                } else {
341                    self.with_directory_and_extension(&self.out_dir, ty.extension())
342                }
343            }
344        }
345    }
346
347    /// Constructs a file path for a temporary file of the given output type, with an optional name,
348    /// falling back to `self.stem` if no name is provided.
349    ///
350    /// The file path is always a child of `self.tmp_dir`
351    pub fn temp_path(&self, ty: OutputType, name: Option<&str>) -> PathBuf {
352        let name = escape_path_component(name.unwrap_or(self.stem.as_str()));
353        self.tmp_dir.join(name.as_ref()).with_extension(ty.extension())
354    }
355
356    /// Build a file path which is either:
357    ///
358    /// * If `self.out_file` is set to a real path, returns it with extension set to `extension`
359    /// * Otherwise, calls [Self::with_directory_and_extension] with `self.out_dir` and `extension`
360    pub fn with_extension(&self, extension: &str) -> PathBuf {
361        match self.out_file.as_ref() {
362            Some(OutputFile::Real(path)) => path.with_extension(extension),
363            Some(OutputFile::Directory(dir)) => {
364                let dir = if dir.is_absolute() {
365                    dir.clone()
366                } else {
367                    self.cwd.join(dir)
368                };
369                self.with_directory_and_extension(&dir, extension)
370            }
371            Some(OutputFile::Stdout) | None => {
372                self.with_directory_and_extension(&self.out_dir, extension)
373            }
374        }
375    }
376
377    /// Build a file path whose parent is `directory`, file stem is `self.stem`, and extension is
378    /// `extension`
379    #[inline]
380    pub fn with_directory_and_extension(&self, directory: &Path, extension: &str) -> PathBuf {
381        let stem = escape_path_component(&self.stem);
382        directory.join(stem.as_ref()).with_extension(extension)
383    }
384}
385
386#[derive(Debug, Clone, Default)]
387pub struct OutputTypes(BTreeMap<OutputType, Option<OutputFile>>);
388impl OutputTypes {
389    #[cfg(feature = "std")]
390    pub fn new<I: IntoIterator<Item = OutputTypeSpec>>(entries: I) -> Result<Self, clap::Error> {
391        let entries = entries.into_iter();
392        let mut map = BTreeMap::default();
393        for spec in entries {
394            match spec {
395                OutputTypeSpec::All { path } => {
396                    if !map.is_empty() {
397                        return Err(clap::Error::raw(
398                            clap::error::ErrorKind::ValueValidation,
399                            "--emit=all cannot be combined with other --emit types",
400                        ));
401                    }
402                    let path = match path {
403                        None => None,
404                        Some(OutputFile::Real(path)) => {
405                            if path.extension().is_some() {
406                                return Err(clap::Error::raw(
407                                    clap::error::ErrorKind::ValueValidation,
408                                    "invalid path for --emit=all: must be a directory",
409                                ));
410                            }
411                            Some(OutputFile::Directory(path))
412                        }
413                        Some(OutputFile::Directory(path)) => {
414                            if path.extension().is_some() {
415                                return Err(clap::Error::raw(
416                                    clap::error::ErrorKind::ValueValidation,
417                                    "invalid path for --emit=all: must be a directory",
418                                ));
419                            }
420                            Some(OutputFile::Directory(path))
421                        }
422                        Some(OutputFile::Stdout) => Some(OutputFile::Stdout),
423                    };
424                    for &ty in OutputType::all() {
425                        map.insert(ty, path.clone());
426                    }
427                }
428                OutputTypeSpec::Subset { output_types, path } => {
429                    // Emit a bundle of output types into the same destination.
430                    for output_type in output_types {
431                        match map.get(&output_type) {
432                            // If the user already chose an explicit destination for this type,
433                            // don't allow `ir`/`inter` to override it.
434                            Some(Some(_)) => {
435                                return Err(clap::Error::raw(
436                                    clap::error::ErrorKind::ValueValidation,
437                                    format!(
438                                        "conflicting --emit options given for output type \
439                                         '{output_type}'"
440                                    ),
441                                ));
442                            }
443                            _ => {
444                                // If the user requested the type without a destination, or hasn't
445                                // requested it at all yet, route it to the `ir` directory.
446                                map.insert(output_type, path.clone());
447                            }
448                        }
449                    }
450                }
451                OutputTypeSpec::Typed { output_type, path } => {
452                    if path.is_some() {
453                        if matches!(map.get(&output_type), Some(Some(_))) {
454                            return Err(clap::Error::raw(
455                                clap::error::ErrorKind::ValueValidation,
456                                format!(
457                                    "conflicting --emit options given for output type \
458                                     '{output_type}'"
459                                ),
460                            ));
461                        }
462                    } else if matches!(map.get(&output_type), Some(Some(_))) {
463                        continue;
464                    }
465                    map.insert(output_type, path);
466                }
467            }
468        }
469        Ok(Self(map))
470    }
471
472    pub fn get(&self, key: &OutputType) -> Option<&Option<OutputFile>> {
473        self.0.get(key)
474    }
475
476    pub fn insert(&mut self, key: OutputType, value: Option<OutputFile>) {
477        self.0.insert(key, value);
478    }
479
480    pub fn clear(&mut self) {
481        self.0.clear();
482    }
483
484    pub fn contains_key(&self, key: &OutputType) -> bool {
485        self.0.contains_key(key)
486    }
487
488    pub fn iter(&self) -> impl Iterator<Item = (&OutputType, &Option<OutputFile>)> + '_ {
489        self.0.iter()
490    }
491
492    pub fn keys(&self) -> impl Iterator<Item = OutputType> + '_ {
493        self.0.keys().copied()
494    }
495
496    pub fn values(&self) -> impl Iterator<Item = Option<&OutputFile>> {
497        self.0.values().map(|v| v.as_ref())
498    }
499
500    #[inline(always)]
501    pub fn is_empty(&self) -> bool {
502        self.0.is_empty()
503    }
504
505    pub fn len(&self) -> usize {
506        self.0.len()
507    }
508
509    pub fn should_link(&self) -> bool {
510        self.0.keys().any(|k| {
511            matches!(k, OutputType::Hir | OutputType::Masm | OutputType::Mast | OutputType::Masp)
512        })
513    }
514
515    pub fn should_codegen(&self) -> bool {
516        self.0
517            .keys()
518            .any(|k| matches!(k, OutputType::Masm | OutputType::Mast | OutputType::Masp))
519    }
520
521    pub fn should_assemble(&self) -> bool {
522        self.0.keys().any(|k| matches!(k, OutputType::Mast | OutputType::Masp))
523    }
524}
525
526/// This type describes an output type with optional path specification
527#[derive(Debug, Clone)]
528pub enum OutputTypeSpec {
529    All {
530        path: Option<OutputFile>,
531    },
532    /// Emit a set of output types to a common destination (typically a directory).
533    ///
534    /// This is primarily intended for shorthand specifications like `--emit=ir[=PATH]`, but can
535    /// represent any two-or-more output types with a shared destination.
536    Subset {
537        output_types: SmallVec<[OutputType; 3]>,
538        path: Option<OutputFile>,
539    },
540    Typed {
541        output_type: OutputType,
542        path: Option<OutputFile>,
543    },
544}
545
546#[cfg(feature = "std")]
547impl clap::builder::ValueParserFactory for OutputTypeSpec {
548    type Parser = OutputTypeParser;
549
550    fn value_parser() -> Self::Parser {
551        OutputTypeParser
552    }
553}
554
555#[doc(hidden)]
556#[derive(Clone)]
557#[cfg(feature = "std")]
558pub struct OutputTypeParser;
559
560#[cfg(feature = "std")]
561impl clap::builder::TypedValueParser for OutputTypeParser {
562    type Value = OutputTypeSpec;
563
564    fn possible_values(
565        &self,
566    ) -> Option<alloc::boxed::Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
567        use alloc::boxed::Box;
568
569        use clap::builder::PossibleValue;
570        Some(Box::new(
571            [
572                PossibleValue::new("ast").help("Abstract Syntax Tree (text)"),
573                PossibleValue::new("wat").help("WebAssembly text format (text)"),
574                PossibleValue::new("hir").help("High-level Intermediate Representation (text)"),
575                PossibleValue::new("masm").help("Miden Assembly (text)"),
576                PossibleValue::new("mast").help("Merkelized Abstract Syntax Tree (text)"),
577                PossibleValue::new("masp").help("Miden Assembly Package Format (binary)"),
578                PossibleValue::new("ir").help("WAT + HIR + MASM (text, optional directory)"),
579                PossibleValue::new("all").help("All of the above"),
580            ]
581            .into_iter(),
582        ))
583    }
584
585    fn parse_ref(
586        &self,
587        _cmd: &clap::Command,
588        _arg: Option<&clap::Arg>,
589        value: &std::ffi::OsStr,
590    ) -> Result<Self::Value, clap::error::Error> {
591        use clap::error::{Error, ErrorKind};
592
593        let output_type = value.to_str().ok_or_else(|| Error::new(ErrorKind::InvalidUtf8))?;
594
595        let (shorthand, path) = match output_type.split_once('=') {
596            None => (output_type, None),
597            Some((shorthand, "-")) => (shorthand, Some(OutputFile::Stdout)),
598            Some((shorthand, path)) => (shorthand, Some(OutputFile::Real(PathBuf::from(path)))),
599        };
600        if shorthand == "all" {
601            let path = match path {
602                None => None,
603                Some(OutputFile::Real(path)) => Some(OutputFile::Directory(path)),
604                Some(OutputFile::Stdout) => Some(OutputFile::Stdout),
605                Some(OutputFile::Directory(_)) => unreachable!("all path is parsed as real"),
606            };
607            return Ok(OutputTypeSpec::All { path });
608        }
609        if shorthand == "ir" {
610            let path = match path {
611                None => None,
612                Some(OutputFile::Real(path)) => Some(OutputFile::Directory(path)),
613                Some(OutputFile::Stdout) => {
614                    return Err(Error::raw(
615                        ErrorKind::InvalidValue,
616                        format!("invalid output type: `{shorthand}=-` - expected `ir[=PATH]`"),
617                    ));
618                }
619                Some(OutputFile::Directory(_)) => unreachable!("ir path is parsed as real"),
620            };
621            let output_types = SmallVec::from_slice(OutputType::ir());
622            return Ok(OutputTypeSpec::Subset { output_types, path });
623        }
624        let output_type = shorthand.parse::<OutputType>().map_err(|_| {
625            Error::raw(
626                ErrorKind::InvalidValue,
627                format!(
628                    "invalid output type: `{shorthand}` - expected one of: {display}, `all`, \
629                     `ir[=PATH]`",
630                    display = OutputType::shorthand_display(),
631                ),
632            )
633        })?;
634        Ok(OutputTypeSpec::Typed { output_type, path })
635    }
636}
637
638#[cfg(feature = "std")]
639trait PathMut {
640    fn with_stem(self, stem: impl AsRef<std::ffi::OsStr>) -> PathBuf;
641    fn with_stem_and_extension(
642        self,
643        stem: impl AsRef<std::ffi::OsStr>,
644        ext: impl AsRef<std::ffi::OsStr>,
645    ) -> PathBuf;
646}
647#[cfg(feature = "std")]
648impl PathMut for &std::path::Path {
649    fn with_stem(self, stem: impl AsRef<std::ffi::OsStr>) -> std::path::PathBuf {
650        let mut path = self.with_file_name(stem);
651        if let Some(ext) = self.extension() {
652            path.set_extension(ext);
653        }
654        path
655    }
656
657    fn with_stem_and_extension(
658        self,
659        stem: impl AsRef<std::ffi::OsStr>,
660        ext: impl AsRef<std::ffi::OsStr>,
661    ) -> std::path::PathBuf {
662        let mut path = self.with_file_name(stem);
663        path.set_extension(ext);
664        path
665    }
666}
667#[cfg(feature = "std")]
668impl PathMut for std::path::PathBuf {
669    fn with_stem(mut self, stem: impl AsRef<std::ffi::OsStr>) -> std::path::PathBuf {
670        if let Some(ext) = self.extension() {
671            let ext = ext.to_string_lossy().into_owned();
672            self.with_stem_and_extension(stem, ext)
673        } else {
674            self.set_file_name(stem);
675            self
676        }
677    }
678
679    fn with_stem_and_extension(
680        mut self,
681        stem: impl AsRef<std::ffi::OsStr>,
682        ext: impl AsRef<std::ffi::OsStr>,
683    ) -> std::path::PathBuf {
684        self.set_file_name(stem);
685        self.set_extension(ext);
686        self
687    }
688}