midenc_compile/
compiler.rs

1#[cfg(feature = "std")]
2use alloc::{borrow::ToOwned, format, string::ToString, vec};
3use alloc::{string::String, sync::Arc, vec::Vec};
4#[cfg(feature = "std")]
5use std::ffi::OsString;
6
7#[cfg(feature = "std")]
8use clap::{builder::ArgPredicate, Parser};
9use midenc_session::{
10    diagnostics::{DefaultSourceManager, Emitter},
11    ColorChoice, DebugInfo, InputFile, LinkLibrary, OptLevel, Options, OutputFile, OutputType,
12    OutputTypeSpec, OutputTypes, Path, PathBuf, ProjectType, Session, TargetEnv, Verbosity,
13    Warnings,
14};
15
16/// Compile a program from WebAssembly or Miden IR, to Miden Assembly.
17#[derive(Debug)]
18#[cfg_attr(feature = "std", derive(Parser))]
19#[cfg_attr(feature = "std", command(name = "midenc"))]
20pub struct Compiler {
21    /// Write all intermediate compiler artifacts to `<dir>`
22    ///
23    /// Defaults to a directory named `target/midenc` in the current working directory
24    #[cfg_attr(
25        feature = "std",
26        arg(
27            long,
28            value_name = "DIR",
29            env = "MIDENC_TARGET_DIR",
30            default_value = "target/midenc",
31            help_heading = "Output"
32        )
33    )]
34    pub target_dir: PathBuf,
35    /// The working directory for the compiler
36    ///
37    /// By default this will be the working directory the compiler is executed from
38    #[cfg_attr(
39        feature = "std",
40        arg(long, value_name = "DIR", help_heading = "Output")
41    )]
42    pub working_dir: Option<PathBuf>,
43    /// The path to the root directory of the Miden toolchain libraries
44    ///
45    /// By default this is assumed to be ~/.miden/toolchains/<version>
46    #[cfg_attr(
47        feature = "std",
48        arg(
49            long,
50            value_name = "DIR",
51            env = "MIDENC_SYSROOT",
52            help_heading = "Compiler"
53        )
54    )]
55    pub sysroot: Option<PathBuf>,
56    /// Write compiled output to compiler-chosen filename in `<dir>`
57    #[cfg_attr(
58        feature = "std",
59        arg(
60            long,
61            short = 'O',
62            value_name = "DIR",
63            env = "MIDENC_OUT_DIR",
64            help_heading = "Output"
65        )
66    )]
67    pub output_dir: Option<PathBuf>,
68    /// Write compiled output to `<filename>`
69    #[cfg_attr(
70        feature = "std",
71        arg(long, short = 'o', value_name = "FILENAME", help_heading = "Output")
72    )]
73    pub output_file: Option<PathBuf>,
74    /// Write output to stdout
75    #[cfg_attr(
76        feature = "std",
77        arg(long, conflicts_with("output_file"), help_heading = "Output")
78    )]
79    pub stdout: bool,
80    /// Specify the name of the project being compiled
81    ///
82    /// The default is derived from the name of the first input file, or if reading from stdin,
83    /// the base name of the working directory.
84    #[cfg_attr(feature = "std", arg(
85        long,
86        short = 'n',
87        value_name = "NAME",
88        default_value = None,
89        help_heading = "Diagnostics"
90    ))]
91    pub name: Option<String>,
92    /// Specify what type and level of informational output to emit
93    #[cfg_attr(feature = "std", arg(
94        long = "verbose",
95        short = 'v',
96        value_enum,
97        value_name = "LEVEL",
98        default_value_t = Verbosity::Info,
99        default_missing_value = "debug",
100        num_args(0..=1),
101        help_heading = "Diagnostics"
102    ))]
103    pub verbosity: Verbosity,
104    /// Specify how warnings should be treated by the compiler.
105    #[cfg_attr(feature = "std", arg(
106        long,
107        short = 'W',
108        value_enum,
109        value_name = "LEVEL",
110        default_value_t = Warnings::All,
111        default_missing_value = "all",
112        num_args(0..=1),
113        help_heading = "Diagnostics"
114    ))]
115    pub warn: Warnings,
116    /// Whether, and how, to color terminal output
117    #[cfg_attr(feature = "std", arg(
118        long,
119        value_enum,
120        default_value_t = ColorChoice::Auto,
121        default_missing_value = "auto",
122        num_args(0..=1),
123        help_heading = "Diagnostics"
124    ))]
125    pub color: ColorChoice,
126    /// The target environment to compile for
127    #[cfg_attr(feature = "std", arg(
128        long,
129        value_name = "TARGET",
130        default_value_t = TargetEnv::Base,
131        help_heading = "Compiler"
132    ))]
133    pub target: TargetEnv,
134    /// Specify the function to call as the entrypoint for the program
135    #[cfg_attr(feature = "std", arg(long, help_heading = "Compiler", hide(true)))]
136    pub entrypoint: Option<String>,
137    /// Tells the compiler to produce an executable Miden program
138    ///
139    /// Implied by `--entrypoint`, defaults to true for non-rollup targets.
140    #[cfg_attr(feature = "std", arg(
141        long = "exe",
142        default_value_t = true,
143        default_value_ifs([
144            // When targeting the rollup, never build an executable
145            ("target", "rollup".into(), Some("false")),
146            // Setting the entrypoint implies building an executable in all other cases
147            ("entrypoint", ArgPredicate::IsPresent, Some("true")),
148        ]),
149        help_heading = "Linker"
150    ))]
151    pub is_program: bool,
152    /// Tells the compiler to produce a Miden library
153    ///
154    /// Implied by `--target rollup`, defaults to false.
155    #[cfg_attr(feature = "std", arg(
156        long = "lib",
157        conflicts_with("is_program"),
158        conflicts_with("entrypoint"),
159        default_value_t = false,
160        default_value_ifs([
161            // When an entrypoint is specified, always set the default to false
162            ("entrypoint", ArgPredicate::IsPresent, Some("false")),
163            // When targeting the rollup, we always build as a library
164            ("target", "rollup".into(), Some("true")),
165        ]),
166        help_heading = "Linker"
167    ))]
168    pub is_library: bool,
169    /// Specify one or more search paths for link libraries requested via `-l`
170    #[cfg_attr(
171        feature = "std",
172        arg(
173            long = "search-path",
174            short = 'L',
175            value_name = "PATH",
176            help_heading = "Linker"
177        )
178    )]
179    pub search_path: Vec<PathBuf>,
180    /// Link compiled projects to the specified library NAME.
181    ///
182    /// The optional KIND can be provided to indicate what type of library it is.
183    ///
184    /// NAME must either be an absolute path (with extension when applicable), or
185    /// a library namespace (no extension). The former will be used as the path
186    /// to load the library, without looking for it in the library search paths,
187    /// while the latter will be located in the search path based on its KIND.
188    ///
189    /// See below for valid KINDs:
190    #[cfg_attr(feature = "std", arg(
191        long = "link-library",
192        short = 'l',
193        value_name = "[KIND=]NAME",
194        value_delimiter = ',',
195        default_value_ifs([
196            ("target", "base", "std"),
197            ("target", "rollup", "std,base"),
198        ]),
199        next_line_help(true),
200        help_heading = "Linker"
201    ))]
202    pub link_libraries: Vec<LinkLibrary>,
203    /// Specify one or more output types for the compiler to emit
204    ///
205    /// The format for SPEC is `KIND[=PATH]`. You can specify multiple items at
206    /// once by separating each SPEC with a comma, you can also pass this flag
207    /// multiple times.
208    ///
209    /// PATH must be a directory in which to place the outputs, or `-` for stdout.
210    #[cfg_attr(
211        feature = "std",
212        arg(
213            long = "emit",
214            value_name = "SPEC",
215            value_delimiter = ',',
216            next_line_help(true),
217            help_heading = "Output"
218        )
219    )]
220    pub output_types: Vec<OutputTypeSpec>,
221    /// Specify what level of debug information to emit in compilation artifacts
222    #[cfg_attr(feature = "std", arg(
223        long,
224        value_enum,
225        value_name = "LEVEL",
226        next_line_help(true),
227        default_value_t = DebugInfo::Full,
228        default_missing_value = "full",
229        num_args(0..=1),
230        help_heading = "Output"
231    ))]
232    pub debug: DebugInfo,
233    /// Specify what type, and to what degree, of optimizations to apply to code during
234    /// compilation.
235    #[cfg_attr(feature = "std", arg(
236        long = "optimize",
237        value_enum,
238        value_name = "LEVEL",
239        next_line_help(true),
240        default_value_t = OptLevel::None,
241        default_missing_value = "balanced",
242        num_args(0..=1),
243        help_heading = "Output"
244    ))]
245    pub opt_level: OptLevel,
246    /// Set a codegen option
247    ///
248    /// Use `-C help` to print available options
249    #[cfg_attr(
250        feature = "std",
251        arg(
252            long,
253            short = 'C',
254            value_name = "OPT[=VALUE]",
255            help_heading = "Compiler"
256        )
257    )]
258    pub codegen: Vec<String>,
259    /// Set an unstable compiler option
260    ///
261    /// Use `-Z help` to print available options
262    #[cfg_attr(
263        feature = "std",
264        arg(
265            long,
266            short = 'Z',
267            value_name = "OPT[=VALUE]",
268            help_heading = "Compiler"
269        )
270    )]
271    pub unstable: Vec<String>,
272}
273
274#[derive(Default, Debug, Clone)]
275#[cfg_attr(feature = "std", derive(Parser))]
276#[cfg_attr(feature = "std", command(name = "-C"))]
277pub struct CodegenOptions {
278    /// Tell the compiler to exit after it has parsed the inputs
279    #[cfg_attr(feature = "std", arg(
280        long,
281        conflicts_with_all(["analyze_only", "link_only"]),
282        default_value_t = false,
283    ))]
284    pub parse_only: bool,
285    /// Tell the compiler to exit after it has performed semantic analysis on the inputs
286    #[cfg_attr(feature = "std", arg(
287        long,
288        conflicts_with_all(["parse_only", "link_only"]),
289        default_value_t = false,
290    ))]
291    pub analyze_only: bool,
292    /// Tell the compiler to exit after linking the inputs, without generating Miden Assembly
293    #[cfg_attr(feature = "std", arg(
294        long,
295        conflicts_with_all(["no_link"]),
296        default_value_t = false,
297    ))]
298    pub link_only: bool,
299    /// Tell the compiler to generate Miden Assembly from the inputs without linking them
300    #[cfg_attr(feature = "std", arg(long, default_value_t = false))]
301    pub no_link: bool,
302}
303
304#[derive(Default, Debug, Clone)]
305#[cfg_attr(feature = "std", derive(Parser))]
306#[cfg_attr(feature = "std", command(name = "-Z"))]
307pub struct UnstableOptions {
308    /// Print the CFG after each HIR pass is applied
309    #[cfg_attr(
310        feature = "std",
311        arg(long, default_value_t = false, help_heading = "Passes")
312    )]
313    pub print_cfg_after_all: bool,
314    /// Print the CFG after running a specific HIR pass
315    #[cfg_attr(
316        feature = "std",
317        arg(
318            long,
319            value_name = "PASS",
320            value_delimiter = ',',
321            help_heading = "Passes"
322        )
323    )]
324    pub print_cfg_after_pass: Vec<String>,
325    /// Print the IR after each pass is applied
326    #[cfg_attr(
327        feature = "std",
328        arg(long, default_value_t = false, help_heading = "Passes")
329    )]
330    pub print_ir_after_all: bool,
331    /// Print the IR after running a specific pass
332    #[cfg_attr(
333        feature = "std",
334        arg(
335            long,
336            value_name = "PASS",
337            value_delimiter = ',',
338            help_heading = "Passes"
339        )
340    )]
341    pub print_ir_after_pass: Vec<String>,
342}
343
344impl CodegenOptions {
345    #[cfg(feature = "std")]
346    fn parse_argv(argv: Vec<String>) -> Self {
347        let command = <CodegenOptions as clap::CommandFactory>::command()
348            .no_binary_name(true)
349            .arg_required_else_help(false)
350            .help_template(
351                "\
352Available codegen options:
353
354Usage: midenc compile -C <opt>
355
356{all-args}{after-help}
357
358NOTE: When specifying these options, strip the leading '--'",
359            );
360
361        let argv = if argv.iter().any(|arg| matches!(arg.as_str(), "--help" | "-h" | "help")) {
362            vec!["--help".to_string()]
363        } else {
364            argv.into_iter()
365                .flat_map(|arg| match arg.split_once('=') {
366                    None => vec![format!("--{arg}")],
367                    Some((opt, value)) => {
368                        vec![format!("--{opt}"), value.to_string()]
369                    }
370                })
371                .collect::<Vec<_>>()
372        };
373
374        let mut matches = command.try_get_matches_from(argv).unwrap_or_else(|err| err.exit());
375        <CodegenOptions as clap::FromArgMatches>::from_arg_matches_mut(&mut matches)
376            .map_err(format_error::<CodegenOptions>)
377            .unwrap_or_else(|err| err.exit())
378    }
379
380    #[cfg(not(feature = "std"))]
381    fn parse_argv(_argv: Vec<String>) -> Self {
382        Self::default()
383    }
384}
385
386impl UnstableOptions {
387    #[cfg(feature = "std")]
388    fn parse_argv(argv: Vec<String>) -> Self {
389        let command = <UnstableOptions as clap::CommandFactory>::command()
390            .no_binary_name(true)
391            .arg_required_else_help(false)
392            .help_template(
393                "\
394Available unstable options:
395
396Usage: midenc compile -Z <opt>
397
398{all-args}{after-help}
399
400NOTE: When specifying these options, strip the leading '--'",
401            );
402
403        let argv = if argv.iter().any(|arg| matches!(arg.as_str(), "--help" | "-h" | "help")) {
404            vec!["--help".to_string()]
405        } else {
406            argv.into_iter()
407                .flat_map(|arg| match arg.split_once('=') {
408                    None => vec![format!("--{arg}")],
409                    Some((opt, value)) => {
410                        vec![format!("--{opt}"), value.to_string()]
411                    }
412                })
413                .collect::<Vec<_>>()
414        };
415
416        let mut matches = command.try_get_matches_from(argv).unwrap_or_else(|err| err.exit());
417        <UnstableOptions as clap::FromArgMatches>::from_arg_matches_mut(&mut matches)
418            .map_err(format_error::<UnstableOptions>)
419            .unwrap_or_else(|err| err.exit())
420    }
421
422    #[cfg(not(feature = "std"))]
423    fn parse_argv(_argv: Vec<String>) -> Self {
424        Self::default()
425    }
426}
427
428impl Compiler {
429    /// Construct a [Compiler] programatically
430    #[cfg(feature = "std")]
431    pub fn new_session<I, A, S>(inputs: I, emitter: Option<Arc<dyn Emitter>>, argv: A) -> Session
432    where
433        I: IntoIterator<Item = InputFile>,
434        A: IntoIterator<Item = S>,
435        S: Into<std::ffi::OsString> + Clone,
436    {
437        let argv = [OsString::from("midenc")]
438            .into_iter()
439            .chain(argv.into_iter().map(|arg| arg.into()));
440        let command = <Self as clap::CommandFactory>::command();
441        let command = midenc_session::flags::register_flags(command);
442        let mut matches = command.try_get_matches_from(argv).unwrap_or_else(|err| err.exit());
443        let compile_matches = matches.clone();
444
445        let opts = <Self as clap::FromArgMatches>::from_arg_matches_mut(&mut matches)
446            .map_err(format_error::<Self>)
447            .unwrap_or_else(|err| err.exit());
448
449        let inputs = inputs.into_iter().collect();
450        opts.into_session(inputs, emitter).with_extra_flags(compile_matches.into())
451    }
452
453    /// Use this configuration to obtain a [Session] used for compilation
454    pub fn into_session(
455        self,
456        inputs: Vec<InputFile>,
457        emitter: Option<Arc<dyn Emitter>>,
458    ) -> Session {
459        let cwd = self.working_dir.unwrap_or_else(current_dir);
460
461        log::trace!(target: "driver", "current working directory = {}", cwd.display());
462
463        // Determine if a specific output file has been requested
464        let output_file = match self.output_file {
465            Some(path) => Some(OutputFile::Real(path)),
466            None if self.stdout => Some(OutputFile::Stdout),
467            None => None,
468        };
469
470        // Initialize output types
471        let mut output_types = OutputTypes::new(self.output_types).unwrap_or_else(|err| err.exit());
472        if output_types.is_empty() {
473            output_types.insert(OutputType::Masp, output_file.clone());
474        } else if output_file.is_some() && output_types.get(&OutputType::Masp).is_some() {
475            // The -o flag overrides --emit
476            output_types.insert(OutputType::Masp, output_file.clone());
477        }
478
479        // Convert --exe or --lib to project type
480        let project_type = if self.is_program {
481            ProjectType::Program
482        } else {
483            ProjectType::Library
484        };
485
486        let codegen = CodegenOptions::parse_argv(self.codegen);
487        let unstable = UnstableOptions::parse_argv(self.unstable);
488
489        // Consolidate all compiler options
490        let mut options = Options::new(self.name, self.target, project_type, cwd, self.sysroot)
491            .with_color(self.color)
492            .with_verbosity(self.verbosity)
493            .with_warnings(self.warn)
494            .with_debug_info(self.debug)
495            .with_optimization(self.opt_level)
496            .with_output_types(output_types);
497        options.search_paths = self.search_path;
498        options.link_libraries = self.link_libraries;
499        options.entrypoint = self.entrypoint;
500        options.parse_only = codegen.parse_only;
501        options.analyze_only = codegen.analyze_only;
502        options.link_only = codegen.link_only;
503        options.no_link = codegen.no_link;
504        options.print_cfg_after_all = unstable.print_cfg_after_all;
505        options.print_cfg_after_pass = unstable.print_cfg_after_pass;
506        options.print_ir_after_all = unstable.print_ir_after_all;
507        options.print_ir_after_pass = unstable.print_ir_after_pass;
508
509        // Establish --target-dir
510        let target_dir = if self.target_dir.is_absolute() {
511            self.target_dir
512        } else {
513            options.current_dir.join(&self.target_dir)
514        };
515        create_target_dir(target_dir.as_path());
516
517        let source_manager = Arc::new(DefaultSourceManager::default());
518        Session::new(
519            inputs,
520            self.output_dir,
521            output_file,
522            target_dir,
523            options,
524            emitter,
525            source_manager,
526        )
527    }
528}
529
530#[cfg(feature = "std")]
531fn format_error<I: clap::CommandFactory>(err: clap::Error) -> clap::Error {
532    let mut cmd = I::command();
533    err.format(&mut cmd)
534}
535
536#[cfg(feature = "std")]
537fn current_dir() -> PathBuf {
538    std::env::current_dir().expect("no working directory available")
539}
540
541#[cfg(not(feature = "std"))]
542fn current_dir() -> PathBuf {
543    <str as AsRef<Path>>::as_ref(".").to_path_buf()
544}
545
546#[cfg(feature = "std")]
547fn create_target_dir(path: &Path) {
548    std::fs::create_dir_all(path)
549        .unwrap_or_else(|err| panic!("unable to create --target-dir '{}': {err}", path.display()));
550}
551
552#[cfg(not(feature = "std"))]
553fn create_target_dir(_path: &Path) {}