midenc_compile/
compiler.rs

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