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}
156
157impl OutputFile {
158    pub fn parent(&self) -> Option<&Path> {
159        match self {
160            Self::Real(path) => path.parent(),
161            Self::Directory(path) => Some(path.as_ref()),
162            Self::Stdout => None,
163        }
164    }
165
166    pub fn filestem(&self) -> Option<Cow<'_, str>> {
167        match self {
168            Self::Real(path) => path.file_stem().map(|stem| stem.to_string_lossy()),
169            Self::Directory(_) => None,
170            Self::Stdout => None,
171        }
172    }
173
174    pub fn is_stdout(&self) -> bool {
175        matches!(self, Self::Stdout)
176    }
177
178    #[cfg(feature = "std")]
179    pub fn is_tty(&self) -> bool {
180        use std::io::IsTerminal;
181        match self {
182            Self::Real(_) => false,
183            Self::Directory(_) => false,
184            Self::Stdout => std::io::stdout().is_terminal(),
185        }
186    }
187
188    #[cfg(not(feature = "std"))]
189    pub fn is_tty(&self) -> bool {
190        false
191    }
192
193    pub fn as_path(&self) -> Option<&Path> {
194        match self {
195            Self::Real(path) => Some(path.as_ref()),
196            Self::Directory(path) => Some(path.as_ref()),
197            Self::Stdout => None,
198        }
199    }
200
201    pub fn file_for_writing(
202        &self,
203        outputs: &OutputFiles,
204        ty: OutputType,
205        name: Option<&str>,
206    ) -> PathBuf {
207        match self {
208            Self::Real(path) => path.clone(),
209            Self::Directory(dir) => {
210                let dir = if dir.is_absolute() {
211                    dir.clone()
212                } else {
213                    outputs.cwd.join(dir)
214                };
215                let stem = escape_path_component(name.unwrap_or(outputs.stem.as_str()));
216                dir.join(stem.as_ref()).with_extension(ty.extension())
217            }
218            Self::Stdout => outputs.temp_path(ty, name),
219        }
220    }
221}
222
223impl fmt::Display for OutputFile {
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        match self {
226            Self::Real(path) => write!(f, "{}", path.display()),
227            Self::Directory(path) => write!(f, "{}", path.display()),
228            Self::Stdout => write!(f, "stdout"),
229        }
230    }
231}
232
233#[derive(Debug, Clone)]
234pub struct OutputFiles {
235    stem: String,
236    /// The compiler working directory
237    pub cwd: PathBuf,
238    /// The directory in which to place temporaries or intermediate artifacts
239    ///
240    /// This is set to the effective `--target-dir`, i.e. `<target_dir>/<profile>`
241    pub tmp_dir: PathBuf,
242    /// The directory in which to place objects produced by the current compiler operation
243    ///
244    /// This directory is intended for non-intermediate artifacts, though it may be used
245    /// to derive `tmp_dir` elsewhere. You should prefer to use `tmp_dir` for files which
246    /// are internal details of the compiler.
247    ///
248    /// This is set to `--output-dir`, or if that is unset, the parent of `--output-file`, and if
249    /// that too is unset, then it is set to the effective `--target-dir`
250    /// (i.e. `<target_dir>/<profile>`).
251    pub out_dir: PathBuf,
252    /// If specified, the specific path at which to write the compiler output.
253    ///
254    /// This _only_ applies to the final output, e.g. the `.masp` package.
255    pub out_file: Option<OutputFile>,
256    /// The raw output types requested by the user on the command line
257    pub outputs: OutputTypes,
258}
259
260impl OutputFiles {
261    pub fn new(
262        stem: String,
263        cwd: PathBuf,
264        out_dir: PathBuf,
265        out_file: Option<OutputFile>,
266        tmp_dir: PathBuf,
267        outputs: OutputTypes,
268    ) -> Self {
269        Self {
270            stem,
271            cwd,
272            tmp_dir,
273            out_dir,
274            out_file,
275            outputs,
276        }
277    }
278
279    /// Return the [OutputFile] representing where an output of `ty` type should be written,
280    /// with an optional `name`, which overrides the file stem of the resulting path, if a
281    /// specific path was not provided.
282    pub fn output_file(&self, ty: OutputType, name: Option<&str>) -> OutputFile {
283        let requested = self.outputs.contains_key(&ty);
284        let default_name = escape_path_component(name.unwrap_or(self.stem.as_str()));
285        match self.outputs.get(&ty).and_then(|p| p.to_owned()) {
286            Some(OutputFile::Real(path)) => OutputFile::Real({
287                let path = if path.is_absolute() {
288                    path
289                } else {
290                    self.cwd.join(path)
291                };
292                if path.is_dir() {
293                    path.join(default_name.as_ref()).with_extension(ty.extension())
294                } else {
295                    path
296                }
297            }),
298            Some(OutputFile::Directory(dir)) => OutputFile::Real({
299                let dir = if dir.is_absolute() {
300                    dir
301                } else {
302                    self.cwd.join(dir)
303                };
304                dir.join(default_name.as_ref()).with_extension(ty.extension())
305            }),
306            Some(OutputFile::Stdout) => OutputFile::Stdout,
307            None => {
308                // If the user requested an output type without specifying a destination, default to
309                // the session output directory. Only compiler-internal temporaries use `tmp_dir`,
310                // except when the session output directory and `--target-dir` are the same.
311                let out = if ty.is_intermediate() {
312                    if requested {
313                        self.with_directory_and_extension(&self.out_dir, ty.extension())
314                    } else {
315                        self.with_directory_and_extension(&self.tmp_dir, ty.extension())
316                    }
317                } else if let Some(output_file) = self.out_file.as_ref() {
318                    return output_file.clone();
319                } else {
320                    self.with_directory_and_extension(&self.out_dir, ty.extension())
321                };
322                OutputFile::Real(if let Some(name) = name {
323                    let name = escape_path_component(name);
324                    out.with_stem(name.as_ref())
325                } else {
326                    out
327                })
328            }
329        }
330    }
331
332    /// Return the most appropriate file path for an output of `ty` type.
333    ///
334    /// The returned path _may_ be precise, if a specific file path was chosen by the user for
335    /// the given output type, but in general the returned path will be derived from the current
336    /// `self.stem`, and is thus an appropriate default path for the given output.
337    pub fn output_path(&self, ty: OutputType) -> PathBuf {
338        match self.output_file(ty, None) {
339            OutputFile::Real(path) => path,
340            OutputFile::Directory(_) => {
341                unreachable!("OutputFiles::output_file never returns OutputFile::Directory")
342            }
343            OutputFile::Stdout => {
344                if ty.is_intermediate() {
345                    self.with_directory_and_extension(&self.tmp_dir, ty.extension())
346                } else if let Some(output_file) = self.out_file.as_ref().and_then(|of| of.as_path())
347                {
348                    output_file.to_path_buf()
349                } else {
350                    self.with_directory_and_extension(&self.out_dir, ty.extension())
351                }
352            }
353        }
354    }
355
356    /// Constructs a file path for a temporary file of the given output type, with an optional name,
357    /// falling back to `self.stem` if no name is provided.
358    ///
359    /// The file path is always a child of `self.tmp_dir`
360    pub fn temp_path(&self, ty: OutputType, name: Option<&str>) -> PathBuf {
361        let name = escape_path_component(name.unwrap_or(self.stem.as_str()));
362        self.tmp_dir.join(name.as_ref()).with_extension(ty.extension())
363    }
364
365    /// Build a file path which is either:
366    ///
367    /// * If `self.out_file` is set to a real path, returns it with extension set to `extension`
368    /// * Otherwise, calls [Self::with_directory_and_extension] with `self.out_dir` and `extension`
369    pub fn with_extension(&self, extension: &str) -> PathBuf {
370        match self.out_file.as_ref() {
371            Some(OutputFile::Real(path)) => path.with_extension(extension),
372            Some(OutputFile::Directory(dir)) => {
373                let dir = if dir.is_absolute() {
374                    dir.clone()
375                } else {
376                    self.cwd.join(dir)
377                };
378                self.with_directory_and_extension(&dir, extension)
379            }
380            Some(OutputFile::Stdout) | None => {
381                self.with_directory_and_extension(&self.out_dir, extension)
382            }
383        }
384    }
385
386    /// Build a file path whose parent is `directory`, file stem is `self.stem`, and extension is
387    /// `extension`
388    #[inline]
389    pub fn with_directory_and_extension(&self, directory: &Path, extension: &str) -> PathBuf {
390        let stem = escape_path_component(&self.stem);
391        directory.join(stem.as_ref()).with_extension(extension)
392    }
393}
394
395#[derive(Debug, Clone, Default)]
396pub struct OutputTypes(BTreeMap<OutputType, Option<OutputFile>>);
397
398impl OutputTypes {
399    #[cfg(feature = "std")]
400    pub fn new<I: IntoIterator<Item = OutputTypeSpec>>(entries: I) -> Result<Self, clap::Error> {
401        let entries = entries.into_iter();
402        let mut map = BTreeMap::default();
403        for spec in entries {
404            match spec {
405                OutputTypeSpec::All { path } => {
406                    if !map.is_empty() {
407                        return Err(clap::Error::raw(
408                            clap::error::ErrorKind::ValueValidation,
409                            "--emit=all cannot be combined with other --emit types",
410                        ));
411                    }
412                    let path = match path {
413                        None => None,
414                        Some(OutputFile::Real(path)) => {
415                            if path.extension().is_some() {
416                                return Err(clap::Error::raw(
417                                    clap::error::ErrorKind::ValueValidation,
418                                    "invalid path for --emit=all: must be a directory",
419                                ));
420                            }
421                            Some(OutputFile::Directory(path))
422                        }
423                        Some(OutputFile::Directory(path)) => {
424                            if path.extension().is_some() {
425                                return Err(clap::Error::raw(
426                                    clap::error::ErrorKind::ValueValidation,
427                                    "invalid path for --emit=all: must be a directory",
428                                ));
429                            }
430                            Some(OutputFile::Directory(path))
431                        }
432                        Some(OutputFile::Stdout) => Some(OutputFile::Stdout),
433                    };
434                    for &ty in OutputType::all() {
435                        map.insert(ty, path.clone());
436                    }
437                }
438                OutputTypeSpec::Subset { output_types, path } => {
439                    // Emit a bundle of output types into the same destination.
440                    for output_type in output_types {
441                        match map.get(&output_type) {
442                            // If the user already chose an explicit destination for this type,
443                            // don't allow `ir`/`inter` to override it.
444                            Some(Some(_)) => {
445                                return Err(clap::Error::raw(
446                                    clap::error::ErrorKind::ValueValidation,
447                                    format!(
448                                        "conflicting --emit options given for output type \
449                                         '{output_type}'"
450                                    ),
451                                ));
452                            }
453                            _ => {
454                                // If the user requested the type without a destination, or hasn't
455                                // requested it at all yet, route it to the `ir` directory.
456                                map.insert(output_type, path.clone());
457                            }
458                        }
459                    }
460                }
461                OutputTypeSpec::Typed { output_type, path } => {
462                    if path.is_some() {
463                        if matches!(map.get(&output_type), Some(Some(_))) {
464                            return Err(clap::Error::raw(
465                                clap::error::ErrorKind::ValueValidation,
466                                format!(
467                                    "conflicting --emit options given for output type \
468                                     '{output_type}'"
469                                ),
470                            ));
471                        }
472                    } else if matches!(map.get(&output_type), Some(Some(_))) {
473                        continue;
474                    }
475                    map.insert(output_type, path);
476                }
477            }
478        }
479        Ok(Self(map))
480    }
481
482    pub fn get(&self, key: &OutputType) -> Option<&Option<OutputFile>> {
483        self.0.get(key)
484    }
485
486    pub fn insert(&mut self, key: OutputType, value: Option<OutputFile>) {
487        self.0.insert(key, value);
488    }
489
490    pub fn clear(&mut self) {
491        self.0.clear();
492    }
493
494    pub fn contains_key(&self, key: &OutputType) -> bool {
495        self.0.contains_key(key)
496    }
497
498    pub fn iter(&self) -> impl Iterator<Item = (&OutputType, &Option<OutputFile>)> + '_ {
499        self.0.iter()
500    }
501
502    pub fn keys(&self) -> impl Iterator<Item = OutputType> + '_ {
503        self.0.keys().copied()
504    }
505
506    pub fn values(&self) -> impl Iterator<Item = Option<&OutputFile>> {
507        self.0.values().map(|v| v.as_ref())
508    }
509
510    #[inline(always)]
511    pub fn is_empty(&self) -> bool {
512        self.0.is_empty()
513    }
514
515    pub fn len(&self) -> usize {
516        self.0.len()
517    }
518
519    pub fn should_link(&self) -> bool {
520        self.0.keys().any(|k| {
521            matches!(k, OutputType::Hir | OutputType::Masm | OutputType::Mast | OutputType::Masp)
522        })
523    }
524
525    pub fn should_codegen(&self) -> bool {
526        self.0
527            .keys()
528            .any(|k| matches!(k, OutputType::Masm | OutputType::Mast | OutputType::Masp))
529    }
530
531    pub fn should_assemble(&self) -> bool {
532        self.0.keys().any(|k| matches!(k, OutputType::Mast | OutputType::Masp))
533    }
534}
535
536/// This type describes an output type with optional path specification
537#[derive(Debug, Clone)]
538pub enum OutputTypeSpec {
539    All {
540        path: Option<OutputFile>,
541    },
542    /// Emit a set of output types to a common destination (typically a directory).
543    ///
544    /// This is primarily intended for shorthand specifications like `--emit=ir[=PATH]`, but can
545    /// represent any two-or-more output types with a shared destination.
546    Subset {
547        output_types: SmallVec<[OutputType; 3]>,
548        path: Option<OutputFile>,
549    },
550    Typed {
551        output_type: OutputType,
552        path: Option<OutputFile>,
553    },
554}
555
556#[cfg(feature = "std")]
557impl clap::builder::ValueParserFactory for OutputTypeSpec {
558    type Parser = OutputTypeParser;
559
560    fn value_parser() -> Self::Parser {
561        OutputTypeParser
562    }
563}
564
565#[doc(hidden)]
566#[derive(Clone)]
567#[cfg(feature = "std")]
568pub struct OutputTypeParser;
569
570#[cfg(feature = "std")]
571impl clap::builder::TypedValueParser for OutputTypeParser {
572    type Value = OutputTypeSpec;
573
574    fn possible_values(
575        &self,
576    ) -> Option<alloc::boxed::Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
577        use alloc::boxed::Box;
578
579        use clap::builder::PossibleValue;
580        Some(Box::new(
581            [
582                PossibleValue::new("ast").help("Abstract Syntax Tree (text)"),
583                PossibleValue::new("wat").help("WebAssembly text format (text)"),
584                PossibleValue::new("hir").help("High-level Intermediate Representation (text)"),
585                PossibleValue::new("masm").help("Miden Assembly (text)"),
586                PossibleValue::new("mast").help("Merkelized Abstract Syntax Tree (text)"),
587                PossibleValue::new("masp").help("Miden Assembly Package Format (binary)"),
588                PossibleValue::new("ir").help("WAT + HIR + MASM (text, optional directory)"),
589                PossibleValue::new("all").help("All of the above"),
590            ]
591            .into_iter(),
592        ))
593    }
594
595    fn parse_ref(
596        &self,
597        _cmd: &clap::Command,
598        _arg: Option<&clap::Arg>,
599        value: &std::ffi::OsStr,
600    ) -> Result<Self::Value, clap::error::Error> {
601        use clap::error::{Error, ErrorKind};
602
603        let output_type = value.to_str().ok_or_else(|| Error::new(ErrorKind::InvalidUtf8))?;
604
605        let (shorthand, path) = match output_type.split_once('=') {
606            None => (output_type, None),
607            Some((shorthand, "-")) => (shorthand, Some(OutputFile::Stdout)),
608            Some((shorthand, path)) => (shorthand, Some(OutputFile::Real(PathBuf::from(path)))),
609        };
610        if shorthand == "all" {
611            let path = match path {
612                None => None,
613                Some(OutputFile::Real(path)) => Some(OutputFile::Directory(path)),
614                Some(OutputFile::Stdout) => Some(OutputFile::Stdout),
615                Some(OutputFile::Directory(_)) => unreachable!("all path is parsed as real"),
616            };
617            return Ok(OutputTypeSpec::All { path });
618        }
619        if shorthand == "ir" {
620            let path = match path {
621                None => None,
622                Some(OutputFile::Real(path)) => Some(OutputFile::Directory(path)),
623                Some(OutputFile::Stdout) => {
624                    return Err(Error::raw(
625                        ErrorKind::InvalidValue,
626                        format!("invalid output type: `{shorthand}=-` - expected `ir[=PATH]`"),
627                    ));
628                }
629                Some(OutputFile::Directory(_)) => unreachable!("ir path is parsed as real"),
630            };
631            let output_types = SmallVec::from_slice(OutputType::ir());
632            return Ok(OutputTypeSpec::Subset { output_types, path });
633        }
634        let output_type = shorthand.parse::<OutputType>().map_err(|_| {
635            Error::raw(
636                ErrorKind::InvalidValue,
637                format!(
638                    "invalid output type: `{shorthand}` - expected one of: {display}, `all`, \
639                     `ir[=PATH]`",
640                    display = OutputType::shorthand_display(),
641                ),
642            )
643        })?;
644        Ok(OutputTypeSpec::Typed { output_type, path })
645    }
646}
647
648#[cfg(feature = "std")]
649trait PathMut {
650    fn with_stem(self, stem: impl AsRef<std::ffi::OsStr>) -> PathBuf;
651    fn with_stem_and_extension(
652        self,
653        stem: impl AsRef<std::ffi::OsStr>,
654        ext: impl AsRef<std::ffi::OsStr>,
655    ) -> PathBuf;
656}
657#[cfg(feature = "std")]
658impl PathMut for &std::path::Path {
659    fn with_stem(self, stem: impl AsRef<std::ffi::OsStr>) -> std::path::PathBuf {
660        let mut path = self.with_file_name(stem);
661        if let Some(ext) = self.extension() {
662            path.set_extension(ext);
663        }
664        path
665    }
666
667    fn with_stem_and_extension(
668        self,
669        stem: impl AsRef<std::ffi::OsStr>,
670        ext: impl AsRef<std::ffi::OsStr>,
671    ) -> std::path::PathBuf {
672        let mut path = self.with_file_name(stem);
673        path.set_extension(ext);
674        path
675    }
676}
677#[cfg(feature = "std")]
678impl PathMut for std::path::PathBuf {
679    fn with_stem(mut self, stem: impl AsRef<std::ffi::OsStr>) -> std::path::PathBuf {
680        if let Some(ext) = self.extension() {
681            let ext = ext.to_string_lossy().into_owned();
682            self.with_stem_and_extension(stem, ext)
683        } else {
684            self.set_file_name(stem);
685            self
686        }
687    }
688
689    fn with_stem_and_extension(
690        mut self,
691        stem: impl AsRef<std::ffi::OsStr>,
692        ext: impl AsRef<std::ffi::OsStr>,
693    ) -> std::path::PathBuf {
694        self.set_file_name(stem);
695        self.set_extension(ext);
696        self
697    }
698}