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}
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 pub cwd: PathBuf,
236 pub tmp_dir: PathBuf,
238 pub out_dir: PathBuf,
244 pub out_file: Option<OutputFile>,
248 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 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 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 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 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 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 #[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 for output_type in output_types {
431 match map.get(&output_type) {
432 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 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#[derive(Debug, Clone)]
528pub enum OutputTypeSpec {
529 All {
530 path: Option<OutputFile>,
531 },
532 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}