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
13fn 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#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
47pub enum OutputMode {
48 Text,
50 Binary,
52}
53
54#[derive(Debug, Copy, Clone, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
56#[cfg_attr(feature = "std", derive(clap::ValueEnum))]
57pub enum OutputType {
58 Ast,
60 Wat,
62 Hir,
64 Masm,
66 Mast,
68 #[default]
70 Masp,
71}
72impl OutputType {
73 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 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 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 pub cwd: PathBuf,
238 pub tmp_dir: PathBuf,
242 pub out_dir: PathBuf,
252 pub out_file: Option<OutputFile>,
256 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 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 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 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 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 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 #[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 for output_type in output_types {
441 match map.get(&output_type) {
442 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 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#[derive(Debug, Clone)]
538pub enum OutputTypeSpec {
539 All {
540 path: Option<OutputFile>,
541 },
542 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}