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