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::{Parser, builder::ArgPredicate};
9use midenc_session::{
10    ColorChoice, DebugInfo, InputFile, LinkLibrary, OptLevel, Options, OutputFile, OutputType,
11    OutputTypeSpec, OutputTypes, Path, PathBuf, ProjectType, Session, TargetEnv, Verbosity,
12    Warnings, add_target_link_libraries,
13    diagnostics::{DefaultSourceManager, Emitter},
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    /// in the format `<module_name>::<function>`
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    // The following options are primarily used by `cargo miden build` to pass through
275    // familiar Cargo options. They are hidden from `midenc --help` output.
276    /// Build in release mode (used by cargo miden build)
277    #[cfg_attr(feature = "std", arg(long, hide = true, default_value_t = false))]
278    pub release: bool,
279    /// Path to Cargo.toml (used by cargo miden build)
280    #[cfg_attr(feature = "std", arg(long, value_name = "PATH", hide = true))]
281    pub manifest_path: Option<PathBuf>,
282    /// Build all packages in the workspace (used by cargo miden build)
283    #[cfg_attr(feature = "std", arg(long, hide = true, default_value_t = false))]
284    pub workspace: bool,
285    /// Package(s) to build (used by cargo miden build)
286    #[cfg_attr(feature = "std", arg(long, value_name = "SPEC", hide = true))]
287    pub package: Vec<String>,
288}
289
290#[derive(Default, Debug, Clone)]
291#[cfg_attr(feature = "std", derive(Parser))]
292#[cfg_attr(feature = "std", command(name = "-C"))]
293pub struct CodegenOptions {
294    /// Tell the compiler to exit after it has parsed the inputs
295    #[cfg_attr(feature = "std", arg(
296        long,
297        conflicts_with_all(["analyze_only", "link_only"]),
298        default_value_t = false,
299    ))]
300    pub parse_only: bool,
301    /// Tell the compiler to exit after it has performed semantic analysis on the inputs
302    #[cfg_attr(feature = "std", arg(
303        long,
304        conflicts_with_all(["parse_only", "link_only"]),
305        default_value_t = false,
306    ))]
307    pub analyze_only: bool,
308    /// Tell the compiler to exit after linking the inputs, without generating Miden Assembly
309    #[cfg_attr(feature = "std", arg(
310        long,
311        conflicts_with_all(["no_link"]),
312        default_value_t = false,
313    ))]
314    pub link_only: bool,
315    /// Tell the compiler to generate Miden Assembly from the inputs without linking them
316    #[cfg_attr(feature = "std", arg(long, default_value_t = false))]
317    pub no_link: bool,
318}
319
320#[derive(Default, Debug, Clone)]
321#[cfg_attr(feature = "std", derive(Parser))]
322#[cfg_attr(feature = "std", command(name = "-Z"))]
323pub struct UnstableOptions {
324    /// Print the CFG after each HIR pass is applied
325    #[cfg_attr(
326        feature = "std",
327        arg(long, default_value_t = false, help_heading = "Passes")
328    )]
329    pub print_cfg_after_all: bool,
330    /// Print the CFG after running a specific HIR pass
331    #[cfg_attr(
332        feature = "std",
333        arg(
334            long,
335            value_name = "PASS",
336            value_delimiter = ',',
337            help_heading = "Passes"
338        )
339    )]
340    pub print_cfg_after_pass: Vec<String>,
341    /// Print the IR after each pass is applied
342    #[cfg_attr(
343        feature = "std",
344        arg(long, default_value_t = false, help_heading = "Passes")
345    )]
346    pub print_ir_after_all: bool,
347    /// Print the IR after running a specific pass
348    #[cfg_attr(
349        feature = "std",
350        arg(
351            long,
352            value_name = "PASS",
353            value_delimiter = ',',
354            help_heading = "Passes"
355        )
356    )]
357    pub print_ir_after_pass: Vec<String>,
358    /// Only print the IR if the pass modified the IR structure. If this flag is set, and no IR
359    /// filter flag is; then the default behavior is to print the IR after every pass.
360    #[cfg_attr(
361        feature = "std",
362        arg(long, default_value_t = false, help_heading = "Passes")
363    )]
364    pub print_ir_after_modified: bool,
365    /// Print source location information in HIR output
366    ///
367    /// When enabled, HIR output will include #loc() annotations showing the source file,
368    /// line, and column for each operation.
369    #[cfg_attr(
370        feature = "std",
371        arg(
372            long = "print-hir-source-locations",
373            default_value_t = false,
374            help_heading = "Printers"
375        )
376    )]
377    pub print_hir_source_locations: bool,
378    /// Specify path prefixes to try when resolving relative paths from DWARF debug info
379    #[cfg_attr(
380        feature = "std",
381        arg(
382            long = "trim-path-prefix",
383            value_name = "PATH",
384            help_heading = "Debugging"
385        )
386    )]
387    pub trim_path_prefixes: Vec<PathBuf>,
388}
389
390impl CodegenOptions {
391    #[cfg(feature = "std")]
392    fn parse_argv(argv: Vec<String>) -> Self {
393        let command = <CodegenOptions as clap::CommandFactory>::command()
394            .no_binary_name(true)
395            .arg_required_else_help(false)
396            .help_template(
397                "\
398Available codegen options:
399
400Usage: midenc compile -C <opt>
401
402{all-args}{after-help}
403
404NOTE: When specifying these options, strip the leading '--'",
405            );
406
407        let argv = if argv.iter().any(|arg| matches!(arg.as_str(), "--help" | "-h" | "help")) {
408            vec!["--help".to_string()]
409        } else {
410            argv.into_iter()
411                .flat_map(|arg| match arg.split_once('=') {
412                    None => vec![format!("--{arg}")],
413                    Some((opt, value)) => {
414                        vec![format!("--{opt}"), value.to_string()]
415                    }
416                })
417                .collect::<Vec<_>>()
418        };
419
420        let mut matches = command.try_get_matches_from(argv).unwrap_or_else(|err| err.exit());
421        <CodegenOptions as clap::FromArgMatches>::from_arg_matches_mut(&mut matches)
422            .map_err(format_error::<CodegenOptions>)
423            .unwrap_or_else(|err| err.exit())
424    }
425
426    #[cfg(not(feature = "std"))]
427    fn parse_argv(_argv: Vec<String>) -> Self {
428        Self::default()
429    }
430}
431
432impl UnstableOptions {
433    #[cfg(feature = "std")]
434    fn parse_argv(argv: Vec<String>) -> Self {
435        let command = <UnstableOptions as clap::CommandFactory>::command()
436            .no_binary_name(true)
437            .arg_required_else_help(false)
438            .help_template(
439                "\
440Available unstable options:
441
442Usage: midenc compile -Z <opt>
443
444{all-args}{after-help}
445
446NOTE: When specifying these options, strip the leading '--'",
447            );
448
449        let argv = if argv.iter().any(|arg| matches!(arg.as_str(), "--help" | "-h" | "help")) {
450            vec!["--help".to_string()]
451        } else {
452            argv.into_iter()
453                .flat_map(|arg| match arg.split_once('=') {
454                    None => vec![format!("--{arg}")],
455                    Some((opt, value)) => {
456                        vec![format!("--{opt}"), value.to_string()]
457                    }
458                })
459                .collect::<Vec<_>>()
460        };
461
462        let mut matches = command.try_get_matches_from(argv).unwrap_or_else(|err| err.exit());
463        <UnstableOptions as clap::FromArgMatches>::from_arg_matches_mut(&mut matches)
464            .map_err(format_error::<UnstableOptions>)
465            .unwrap_or_else(|err| err.exit())
466    }
467
468    #[cfg(not(feature = "std"))]
469    fn parse_argv(_argv: Vec<String>) -> Self {
470        Self::default()
471    }
472}
473
474impl Compiler {
475    /// Parse compiler options from command-line arguments.
476    ///
477    /// Returns the parsed options or an error if parsing failed.
478    /// This is used by `cargo miden build` to parse all arguments into `Compiler`
479    /// options before selectively forwarding them to `cargo build` and `midenc`.
480    #[cfg(feature = "std")]
481    pub fn try_parse_from<I, T>(iter: I) -> Result<Self, clap::Error>
482    where
483        I: IntoIterator<Item = T>,
484        T: Into<OsString> + Clone,
485    {
486        let argv = [OsString::from("midenc")]
487            .into_iter()
488            .chain(iter.into_iter().map(|arg| arg.into()));
489        let command = <Self as clap::CommandFactory>::command();
490        let command = midenc_session::flags::register_flags(command);
491        let mut matches = command.try_get_matches_from(argv)?;
492
493        <Self as clap::FromArgMatches>::from_arg_matches_mut(&mut matches)
494            .map_err(format_error::<Self>)
495    }
496
497    /// Construct a [Compiler] programatically
498    #[cfg(feature = "std")]
499    pub fn new_session<I, A, S>(inputs: I, emitter: Option<Arc<dyn Emitter>>, argv: A) -> Session
500    where
501        I: IntoIterator<Item = InputFile>,
502        A: IntoIterator<Item = S>,
503        S: Into<std::ffi::OsString> + Clone,
504    {
505        let argv = [OsString::from("midenc")]
506            .into_iter()
507            .chain(argv.into_iter().map(|arg| arg.into()));
508        let command = <Self as clap::CommandFactory>::command();
509        let command = midenc_session::flags::register_flags(command);
510        let mut matches = command.try_get_matches_from(argv).unwrap_or_else(|err| err.exit());
511        let compile_matches = matches.clone();
512
513        let opts = <Self as clap::FromArgMatches>::from_arg_matches_mut(&mut matches)
514            .map_err(format_error::<Self>)
515            .unwrap_or_else(|err| err.exit());
516
517        let inputs = inputs.into_iter().collect();
518        opts.into_session(inputs, emitter).with_extra_flags(compile_matches.into())
519    }
520
521    /// Use this configuration to obtain a [Session] used for compilation
522    pub fn into_session(
523        self,
524        inputs: Vec<InputFile>,
525        emitter: Option<Arc<dyn Emitter>>,
526    ) -> Session {
527        let cwd = self.working_dir.unwrap_or_else(current_dir);
528
529        log::trace!(target: "driver", "current working directory = {}", cwd.display());
530
531        // Determine if a specific output file has been requested
532        let output_file = match self.output_file {
533            Some(path) => Some(OutputFile::Real(path)),
534            None if self.stdout => Some(OutputFile::Stdout),
535            None => None,
536        };
537
538        // Initialize output types
539        let mut output_types = OutputTypes::new(self.output_types).unwrap_or_else(|err| err.exit());
540        if output_types.is_empty() {
541            output_types.insert(OutputType::Masp, output_file.clone());
542        } else if output_file.is_some() && output_types.get(&OutputType::Masp).is_some() {
543            // The -o flag overrides --emit
544            output_types.insert(OutputType::Masp, output_file.clone());
545        }
546
547        // Convert --exe or --lib to project type
548        let project_type = if self.is_program {
549            ProjectType::Program
550        } else {
551            ProjectType::Library
552        };
553
554        let codegen = CodegenOptions::parse_argv(self.codegen);
555        let unstable = UnstableOptions::parse_argv(self.unstable);
556
557        // Consolidate all compiler options
558        let mut options = Options::new(self.name, self.target, project_type, cwd, self.sysroot)
559            .with_color(self.color)
560            .with_verbosity(self.verbosity)
561            .with_warnings(self.warn)
562            .with_debug_info(self.debug)
563            .with_optimization(self.opt_level)
564            .with_output_types(output_types);
565        options.search_paths = self.search_path;
566        let link_libraries = add_target_link_libraries(self.link_libraries, &self.target);
567        options.link_libraries = link_libraries;
568        options.entrypoint = self.entrypoint;
569        options.parse_only = codegen.parse_only;
570        options.analyze_only = codegen.analyze_only;
571        options.link_only = codegen.link_only;
572        options.no_link = codegen.no_link;
573        options.print_cfg_after_all = unstable.print_cfg_after_all;
574        options.print_cfg_after_pass = unstable.print_cfg_after_pass;
575        options.print_ir_after_all = unstable.print_ir_after_all;
576        options.print_ir_after_pass = unstable.print_ir_after_pass;
577        options.print_ir_after_modified = unstable.print_ir_after_modified;
578        options.print_hir_source_locations = unstable.print_hir_source_locations;
579        options.trim_path_prefixes = unstable.trim_path_prefixes;
580
581        // Establish --target-dir
582        let target_dir = if self.target_dir.is_absolute() {
583            self.target_dir
584        } else {
585            options.current_dir.join(&self.target_dir)
586        };
587        create_target_dir(target_dir.as_path());
588
589        let source_manager = Arc::new(DefaultSourceManager::default());
590        Session::new(
591            inputs,
592            self.output_dir,
593            output_file,
594            target_dir,
595            options,
596            emitter,
597            source_manager,
598        )
599    }
600}
601
602#[cfg(feature = "std")]
603fn format_error<I: clap::CommandFactory>(err: clap::Error) -> clap::Error {
604    let mut cmd = I::command();
605    err.format(&mut cmd)
606}
607
608#[cfg(feature = "std")]
609fn current_dir() -> PathBuf {
610    std::env::current_dir().expect("no working directory available")
611}
612
613#[cfg(not(feature = "std"))]
614fn current_dir() -> PathBuf {
615    <str as AsRef<Path>>::as_ref(".").to_path_buf()
616}
617
618#[cfg(feature = "std")]
619fn create_target_dir(path: &Path) {
620    std::fs::create_dir_all(path)
621        .unwrap_or_else(|err| panic!("unable to create --target-dir '{}': {err}", path.display()));
622}
623
624#[cfg(not(feature = "std"))]
625fn create_target_dir(_path: &Path) {}