Skip to main content

midenc_session/
inputs.rs

1#[cfg(feature = "std")]
2use alloc::format;
3use alloc::{borrow::Cow, string::String, vec, vec::Vec};
4use core::fmt;
5
6use crate::{Path, PathBuf};
7
8#[derive(Clone)]
9pub struct FileName {
10    name: Cow<'static, str>,
11    is_path: bool,
12}
13impl Eq for FileName {}
14impl PartialEq for FileName {
15    fn eq(&self, other: &Self) -> bool {
16        self.name == other.name
17    }
18}
19impl PartialOrd for FileName {
20    #[inline]
21    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
22        Some(self.cmp(other))
23    }
24}
25impl Ord for FileName {
26    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
27        self.name.cmp(&other.name)
28    }
29}
30impl fmt::Debug for FileName {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        write!(f, "{}", self.as_str())
33    }
34}
35impl fmt::Display for FileName {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "{}", self.as_str())
38    }
39}
40impl AsRef<Path> for FileName {
41    fn as_ref(&self) -> &Path {
42        self.name.as_ref().as_ref()
43    }
44}
45impl From<PathBuf> for FileName {
46    fn from(path: PathBuf) -> Self {
47        Self {
48            name: path.to_string_lossy().into_owned().into(),
49            is_path: true,
50        }
51    }
52}
53impl From<&'static str> for FileName {
54    fn from(name: &'static str) -> Self {
55        Self {
56            name: Cow::Borrowed(name),
57            is_path: false,
58        }
59    }
60}
61impl From<String> for FileName {
62    fn from(name: String) -> Self {
63        Self {
64            name: Cow::Owned(name),
65            is_path: false,
66        }
67    }
68}
69impl AsRef<str> for FileName {
70    fn as_ref(&self) -> &str {
71        self.name.as_ref()
72    }
73}
74impl FileName {
75    pub fn is_path(&self) -> bool {
76        self.is_path
77    }
78
79    pub fn as_path(&self) -> &Path {
80        self.as_ref()
81    }
82
83    pub fn as_str(&self) -> &str {
84        self.name.as_ref()
85    }
86
87    pub fn file_name(&self) -> Option<&str> {
88        self.as_path().file_name().and_then(|name| name.to_str())
89    }
90
91    pub fn file_stem(&self) -> Option<&str> {
92        self.as_path().file_stem().and_then(|name| name.to_str())
93    }
94}
95
96/// An error that occurs when detecting the file type of an input
97#[derive(Debug, thiserror::Error)]
98pub enum InvalidInputError {
99    /// Occurs if an unsupported file type is given as an input
100    #[error("invalid input file '{}': unsupported file type", .0.display())]
101    UnsupportedFileType(PathBuf),
102    /// We attempted to detecth the file type from the raw bytes, but failed
103    #[error("could not detect file type of input")]
104    UnrecognizedFileType,
105    /// Unable to read input file
106    #[cfg(feature = "std")]
107    #[error(transparent)]
108    Io(#[from] std::io::Error),
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub enum InputType {
113    Real(PathBuf),
114    Stdin { name: FileName, input: Vec<u8> },
115}
116
117/// This enum represents the types of raw inputs provided to the compiler
118#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct InputFile {
120    pub file: InputType,
121    file_type: FileType,
122}
123impl InputFile {
124    pub fn new(ty: FileType, file: InputType) -> Self {
125        Self {
126            file,
127            file_type: ty,
128        }
129    }
130
131    /// Returns an [InputFile] representing an empty WebAssembly module binary
132    pub fn empty() -> Self {
133        Self {
134            file: InputType::Stdin {
135                name: "empty".into(),
136                input: vec![],
137            },
138            file_type: FileType::Wasm,
139        }
140    }
141
142    /// Get an [InputFile] representing the contents of `path`.
143    ///
144    /// This function returns an error if the contents are not a valid supported file type.
145    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, InvalidInputError> {
146        let path = path.as_ref();
147        let file_type = FileType::try_from(path)?;
148        Ok(Self {
149            file: InputType::Real(path.to_path_buf()),
150            file_type,
151        })
152    }
153
154    /// Get an [InputFile] representing the contents received from standard input.
155    ///
156    /// This function returns an error if the contents are not a valid supported file type.
157    #[cfg(feature = "std")]
158    pub fn from_stdin(name: FileName) -> Result<Self, InvalidInputError> {
159        use std::io::Read;
160
161        let mut input = Vec::with_capacity(1024);
162        std::io::stdin().read_to_end(&mut input)?;
163        Self::from_bytes(input, name)
164    }
165
166    pub fn from_bytes(bytes: Vec<u8>, name: FileName) -> Result<Self, InvalidInputError> {
167        let file_type = FileType::detect(&bytes)?;
168        Ok(Self {
169            file: InputType::Stdin { name, input: bytes },
170            file_type,
171        })
172    }
173
174    pub fn file_type(&self) -> FileType {
175        self.file_type
176    }
177
178    pub fn file_name(&self) -> FileName {
179        match &self.file {
180            InputType::Real(path) => path.clone().into(),
181            InputType::Stdin { name, .. } => name.clone(),
182        }
183    }
184
185    pub fn as_path(&self) -> Option<&Path> {
186        match &self.file {
187            InputType::Real(path) => Some(path),
188            _ => None,
189        }
190    }
191
192    pub fn is_real(&self) -> bool {
193        matches!(self.file, InputType::Real(_))
194    }
195
196    pub fn filestem(&self) -> &str {
197        match &self.file {
198            InputType::Real(path) => path.file_stem().unwrap().to_str().unwrap(),
199            InputType::Stdin { .. } => "noname",
200        }
201    }
202}
203
204#[cfg(feature = "std")]
205impl clap::builder::ValueParserFactory for InputFile {
206    type Parser = InputFileParser;
207
208    fn value_parser() -> Self::Parser {
209        InputFileParser
210    }
211}
212
213#[doc(hidden)]
214#[derive(Clone)]
215#[cfg(feature = "std")]
216pub struct InputFileParser;
217
218#[cfg(feature = "std")]
219impl clap::builder::TypedValueParser for InputFileParser {
220    type Value = InputFile;
221
222    fn parse_ref(
223        &self,
224        _cmd: &clap::Command,
225        _arg: Option<&clap::Arg>,
226        value: &std::ffi::OsStr,
227    ) -> Result<Self::Value, clap::error::Error> {
228        use clap::error::{Error, ErrorKind};
229
230        let input_file = match value.to_str() {
231            Some("-") => InputFile::from_stdin("stdin".into()).map_err(|err| match err {
232                InvalidInputError::Io(err) => Error::raw(ErrorKind::Io, err),
233                err => Error::raw(ErrorKind::ValueValidation, err),
234            })?,
235            Some(_) | None => {
236                InputFile::from_path(PathBuf::from(value)).map_err(|err| match err {
237                    InvalidInputError::Io(err) => Error::raw(ErrorKind::Io, err),
238                    err => Error::raw(ErrorKind::ValueValidation, err),
239                })?
240            }
241        };
242
243        match &input_file.file {
244            InputType::Real(path) => {
245                if path.exists() {
246                    if path.is_file() {
247                        Ok(input_file)
248                    } else {
249                        Err(Error::raw(
250                            ErrorKind::ValueValidation,
251                            format!("invalid input '{}': not a file", path.display()),
252                        ))
253                    }
254                } else {
255                    Err(Error::raw(
256                        ErrorKind::ValueValidation,
257                        format!("invalid input '{}': file does not exist", path.display()),
258                    ))
259                }
260            }
261            InputType::Stdin { .. } => Ok(input_file),
262        }
263    }
264}
265
266/// This represents the file types recognized by the compiler
267#[derive(Debug, Copy, Clone, PartialEq, Eq)]
268pub enum FileType {
269    Hir,
270    Masm,
271    Masp,
272    Rust,
273    Toml,
274    Wasm,
275    Wat,
276}
277
278impl fmt::Display for FileType {
279    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
280        match self {
281            Self::Hir => f.write_str("hir"),
282            Self::Masm => f.write_str("masm"),
283            Self::Masp => f.write_str("masp"),
284            Self::Rust => f.write_str("rs"),
285            Self::Toml => f.write_str("toml"),
286            Self::Wasm => f.write_str("wasm"),
287            Self::Wat => f.write_str("wat"),
288        }
289    }
290}
291
292impl FileType {
293    pub fn detect(bytes: &[u8]) -> Result<Self, InvalidInputError> {
294        if bytes.starts_with(b"\0asm") {
295            return Ok(FileType::Wasm);
296        }
297
298        if bytes.starts_with(b"MASP\0") {
299            return Ok(FileType::Masp);
300        }
301
302        fn is_rust_top_level_item(line: &str) -> bool {
303            line.starts_with("//")
304                || line.starts_with("#[")
305                || line.starts_with("#![")
306                || line.starts_with("pub fn")
307                || line.starts_with("fn ")
308        }
309
310        fn is_masm_top_level_item(line: &str) -> bool {
311            line.starts_with("namespace")
312                || line.starts_with("extern package")
313                || line.starts_with("pub proc")
314                || line.starts_with("proc ")
315                || line.starts_with("adv_map")
316                || line.starts_with("const ")
317                || line.starts_with("begin ")
318        }
319
320        fn is_project_toml_item(line: &str) -> bool {
321            line.starts_with("[workspace]") || line.starts_with("[package]")
322        }
323
324        if let Ok(content) = core::str::from_utf8(bytes) {
325            // Skip comment lines and empty lines
326            let first_line = content.lines().find(|line| {
327                if line.trim().is_empty() {
328                    return false;
329                }
330                if line.starts_with('#') && !(line.starts_with("#[") || line.starts_with("#![")) {
331                    return false;
332                }
333                !line.starts_with(';')
334            });
335            if let Some(first_line) = first_line {
336                if first_line.starts_with("builtin.") {
337                    return Ok(FileType::Hir);
338                }
339                if first_line.starts_with("(module") {
340                    return Ok(FileType::Wat);
341                }
342                if is_rust_top_level_item(first_line) {
343                    return Ok(FileType::Rust);
344                }
345                if is_masm_top_level_item(first_line) {
346                    return Ok(FileType::Masm);
347                }
348                if is_project_toml_item(first_line) {
349                    return Ok(FileType::Toml);
350                }
351            }
352
353            // We could not detect from the first line alone, so fall back to attempting to parse
354            // the most common types - a failure is assumed to mean that the input does not match.
355            //
356            // We try in an order such that the faster checks are performed first - in the case of
357            // Rust, we have to actually invoke `rustc` to try and check if the input parses, so
358            // it is the slowest. We can't actually attempt to parse HIR here, but we also control
359            // its output format, so it is unlikely valid HIR reaches here anyway. We don't attempt
360            // to parse WAT here currently - if the input doesn't look like WAT we'd expect, it
361            // will get rejected.
362            let parsed = miden_assembly_syntax_cst::parse_text(content);
363            if !parsed.has_errors() {
364                return Ok(FileType::Masm);
365            }
366
367            if parses_as_rust(content) {
368                return Ok(FileType::Rust);
369            }
370        }
371
372        Err(InvalidInputError::UnrecognizedFileType)
373    }
374}
375
376impl TryFrom<&Path> for FileType {
377    type Error = InvalidInputError;
378
379    fn try_from(path: &Path) -> Result<Self, Self::Error> {
380        match path.extension().and_then(|ext| ext.to_str()) {
381            Some("hir") => Ok(FileType::Hir),
382            Some("masm") => Ok(FileType::Masm),
383            Some("masp") => Ok(FileType::Masp),
384            Some("rs") => Ok(FileType::Rust),
385            Some("toml") => Ok(FileType::Toml),
386            Some("wasm") => Ok(FileType::Wasm),
387            Some("wat") => Ok(FileType::Wat),
388            _ => Err(InvalidInputError::UnsupportedFileType(path.to_path_buf())),
389        }
390    }
391}
392
393#[cfg(feature = "std")]
394fn parses_as_rust(src: &str) -> bool {
395    use std::{
396        io::Write,
397        process::{Command, Stdio},
398    };
399
400    let mut command = Command::new("rustc");
401    command
402        .arg("+nightly")
403        .arg("-Zno-analysis")
404        .arg("-Zparse-crate-root-only=yes")
405        .arg("-")
406        .stderr(Stdio::null())
407        .stdout(Stdio::null())
408        .stdin(Stdio::piped());
409
410    let Ok(mut child) = command.spawn() else {
411        return false;
412    };
413
414    let Some(mut stdin) = child.stdin.take() else {
415        let _ = child.kill();
416        return false;
417    };
418
419    let Ok(_) = stdin.write_all(src.as_bytes()) else {
420        let _ = child.kill();
421        return false;
422    };
423
424    let Ok(status) = child.wait() else {
425        return false;
426    };
427
428    status.success()
429}
430
431#[cfg(not(feature = "std"))]
432fn parses_as_rust(_src: &str) -> bool {
433    false
434}