Skip to main content

nargo_cli/cli/
compile_cmd.rs

1use std::hash::BuildHasher;
2use std::io::{Read, Write};
3use std::path::Path;
4use std::time::Duration;
5
6use fm::FileManager;
7use nargo::ops::{collect_errors, compile_contract, compile_program, report_errors};
8use nargo::package::Package;
9use nargo::workspace::Workspace;
10use nargo::{insert_all_files_for_workspace_into_file_manager, parse_all};
11use nargo_toml::PackageSelection;
12use noir_artifact_cli::fs::artifact::{
13    read_program_from_file, save_contract_to_file, save_program_to_file,
14};
15use noirc_artifacts::contract::CompiledContract;
16use noirc_driver::NOIR_ARTIFACT_VERSION_STRING;
17use noirc_driver::{CompilationResult, CompileOptions};
18
19use clap::Args;
20use noirc_frontend::hir::ParsedFiles;
21use notify_debouncer_full::new_debouncer;
22use notify_debouncer_full::notify::{EventKind, RecursiveMode};
23
24use crate::errors::CliError;
25
26use super::{LockType, PackageOptions, WorkspaceCommand};
27use rayon::prelude::*;
28
29/// Compile the program and its secret execution trace into ACIR format
30#[derive(Debug, Clone, Args)]
31pub struct CompileCommand {
32    #[clap(flatten)]
33    pub(super) package_options: PackageOptions,
34
35    #[clap(flatten)]
36    pub(super) compile_options: CompileOptions,
37
38    /// Watch workspace and recompile on changes.
39    #[clap(long, hide = true)]
40    watch: bool,
41}
42
43impl WorkspaceCommand for CompileCommand {
44    fn package_selection(&self) -> PackageSelection {
45        self.package_options.package_selection()
46    }
47
48    fn lock_type(&self) -> LockType {
49        LockType::Exclusive
50    }
51}
52
53pub(crate) fn run(args: CompileCommand, workspace: Workspace) -> Result<(), CliError> {
54    if args.watch {
55        if args.compile_options.debug_compile_stdin {
56            return Err(CliError::CantWatchStdin);
57        }
58        watch_workspace(&workspace, &args.compile_options)
59            .map_err(|err| CliError::Generic(err.to_string()))?;
60    } else {
61        let debug_compile_stdin = None;
62        compile_workspace_full(&workspace, &args.compile_options, debug_compile_stdin)?;
63    }
64    Ok(())
65}
66
67/// Continuously recompile the workspace on any Noir file change event.
68fn watch_workspace(
69    workspace: &Workspace,
70    compile_options: &CompileOptions,
71) -> notify_debouncer_full::notify::Result<()> {
72    let (tx, rx) = std::sync::mpsc::channel();
73
74    // No specific tickrate, max debounce time 1 seconds
75    let mut debouncer = new_debouncer(Duration::from_secs(1), None, tx)?;
76
77    // Add a path to be watched. All files and directories at that path and
78    // below will be monitored for changes.
79    debouncer.watch(&workspace.root_dir, RecursiveMode::Recursive)?;
80
81    let mut screen = std::io::stdout();
82    write!(screen, "{}", termion::cursor::Save).unwrap();
83    screen.flush().unwrap();
84    let debug_compile_stdin = None;
85    let _ = compile_workspace_full(workspace, compile_options, debug_compile_stdin);
86    for res in rx {
87        let debounced_events = res.map_err(|mut err| err.remove(0))?;
88
89        // We only want to trigger a rebuild if a noir source file has been modified.
90        let noir_files_modified = debounced_events.iter().any(|event| {
91            let mut event_paths = event.event.paths.iter();
92            let event_affects_noir_file =
93                event_paths.any(|path| path.extension().is_some_and(|ext| ext == "nr"));
94
95            let is_relevant_event_kind = matches!(
96                event.kind,
97                EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_)
98            );
99
100            is_relevant_event_kind && event_affects_noir_file
101        });
102
103        if noir_files_modified {
104            write!(screen, "{}{}", termion::cursor::Restore, termion::clear::AfterCursor).unwrap();
105            screen.flush().unwrap();
106            let debug_compile_stdin = None;
107            let _ = compile_workspace_full(workspace, compile_options, debug_compile_stdin);
108        }
109    }
110
111    screen.flush().unwrap();
112
113    Ok(())
114}
115
116/// Parse all files in the workspace.
117pub fn parse_workspace(
118    workspace: &Workspace,
119    debug_compile_stdin: Option<String>,
120) -> (FileManager, ParsedFiles) {
121    let mut file_manager = workspace.new_file_manager();
122
123    if let Some(main_nr) = debug_compile_stdin {
124        file_manager.add_file_with_source(Path::new("src/main.nr"), main_nr);
125    } else {
126        insert_all_files_for_workspace_into_file_manager(workspace, &mut file_manager);
127    }
128
129    let parsed_files = parse_all(&file_manager);
130    (file_manager, parsed_files)
131}
132
133/// Parse and compile the entire workspace, then report errors.
134/// This is the main entry point used by all other commands that need compilation.
135pub fn compile_workspace_full(
136    workspace: &Workspace,
137    compile_options: &CompileOptions,
138    debug_compile_stdin: Option<String>, // use this String as STDIN if present
139) -> Result<(), CliError> {
140    let mut debug_compile_stdin = debug_compile_stdin;
141    if compile_options.debug_compile_stdin && debug_compile_stdin.is_none() {
142        let mut main_nr = String::new();
143        let stdin = std::io::stdin();
144        let mut stdin_handle = stdin.lock();
145        stdin_handle.read_to_string(&mut main_nr).expect("reading from stdin to succeed");
146        debug_compile_stdin = Some(main_nr);
147    }
148    let (workspace_file_manager, parsed_files) = parse_workspace(workspace, debug_compile_stdin);
149
150    let compiled_workspace =
151        compile_workspace(&workspace_file_manager, &parsed_files, workspace, compile_options)?;
152
153    report_errors(
154        compiled_workspace,
155        &workspace_file_manager,
156        &parsed_files,
157        compile_options.deny_warnings,
158        compile_options.silence_warnings,
159    )?;
160
161    Ok(())
162}
163
164/// Compile binary and contract packages.
165/// Returns the merged warnings or errors.
166fn compile_workspace(
167    file_manager: &FileManager,
168    parsed_files: &ParsedFiles,
169    workspace: &Workspace,
170    compile_options: &CompileOptions,
171) -> Result<CompilationResult<()>, CliError> {
172    let (binary_packages, contract_packages): (Vec<_>, Vec<_>) = workspace
173        .into_iter()
174        .filter(|package| !package.is_library())
175        .cloned()
176        .partition(|package| package.is_binary());
177
178    // Compile all of the packages in parallel.
179    let program_warnings_or_errors: CompilationResult<()> =
180        compile_programs(file_manager, parsed_files, workspace, &binary_packages, compile_options)?;
181
182    let contract_warnings_or_errors: CompilationResult<()> = compile_contracts(
183        file_manager,
184        parsed_files,
185        &contract_packages,
186        compile_options,
187        &workspace.target_directory_path(),
188    )?;
189
190    let result = match (program_warnings_or_errors, contract_warnings_or_errors) {
191        (Ok((_, program_warnings)), Ok((_, contract_warnings))) => {
192            let warnings = [program_warnings, contract_warnings].concat();
193            Ok(((), warnings))
194        }
195        (Err(program_errors), Err(contract_errors)) => {
196            Err([program_errors, contract_errors].concat())
197        }
198        (Err(errors), _) | (_, Err(errors)) => Err(errors),
199    };
200    Ok(result)
201}
202
203/// Compile the given binary packages in the workspace.
204fn compile_programs(
205    file_manager: &FileManager,
206    parsed_files: &ParsedFiles,
207    workspace: &Workspace,
208    binary_packages: &[Package],
209    compile_options: &CompileOptions,
210) -> Result<CompilationResult<()>, CliError> {
211    // Load any existing artifact for a given package, _iff_ it was compiled with the same nargo version.
212    // The loaded circuit includes backend specific transformations, which might be different from the current target.
213    let load_cached_program = |package| {
214        let program_artifact_path = workspace.package_build_path(package);
215        read_program_from_file(&program_artifact_path)
216            .ok()
217            .filter(|p| p.noir_version == NOIR_ARTIFACT_VERSION_STRING)
218            .map(|p| p.into())
219    };
220
221    let compile_package = |package| -> Result<CompilationResult<()>, CliError> {
222        let cached_program = load_cached_program(package);
223
224        // Hash over the entire compiled program, including any post-compile transformations.
225        // This is used to detect whether `cached_program` is returned by `compile_program`.
226        let cached_hash =
227            cached_program.as_ref().map(|prog| rustc_hash::FxBuildHasher.hash_one(prog));
228
229        // Compile the program, or use the cached artifacts if it matches.
230        match compile_program(
231            file_manager,
232            parsed_files,
233            workspace,
234            package,
235            compile_options,
236            cached_program,
237        ) {
238            Ok((program, warnings)) => {
239                // If the compiled program is the same as the cached one, we don't apply transformations again, unless the target width has changed.
240                // The transformations might not be idempotent, which would risk creating witnesses that don't work with earlier versions,
241                // based on which we might have generated a verifier already.
242                if cached_hash == Some(rustc_hash::FxBuildHasher.hash_one(&program)) {
243                    return Ok(Ok(((), warnings)));
244                }
245                // Run ACVM optimizations.
246                let program = nargo::ops::optimize_program(program);
247                // Check solvability.
248                match nargo::ops::check_program(&program) {
249                    Ok(()) => {
250                        // Overwrite the build artifacts with the final circuit, which includes the backend specific transformations.
251                        let _ = save_program_to_file(
252                            &program.into(),
253                            &package.name,
254                            &workspace.target_directory_path(),
255                        )?;
256                        Ok(Ok(((), warnings)))
257                    }
258                    Err(errors_and_warnings) => Ok(Err(errors_and_warnings)),
259                }
260            }
261            Err(errors_and_warnings) => Ok(Err(errors_and_warnings)),
262        }
263    };
264
265    // Configure a thread pool with a larger stack size to prevent overflowing stack in large programs.
266    // Default is 2MB. Limit threads to the number of packages we actually need to compile.
267    let num_threads = rayon::current_num_threads().min(binary_packages.len()).max(1);
268    let pool = rayon::ThreadPoolBuilder::new()
269        .num_threads(num_threads)
270        .stack_size(4 * 1024 * 1024)
271        .build()
272        .unwrap();
273    let program_results = pool.install(|| {
274        binary_packages.par_iter().map(compile_package).collect::<Result<Vec<_>, _>>()
275    })?;
276
277    // Collate any warnings/errors which were encountered during compilation.
278    Ok(collect_errors(program_results).map(|(_, warnings)| ((), warnings)))
279}
280
281/// Compile the given contracts in the workspace.
282fn compile_contracts(
283    file_manager: &FileManager,
284    parsed_files: &ParsedFiles,
285    contract_packages: &[Package],
286    compile_options: &CompileOptions,
287    target_dir: &Path,
288) -> Result<CompilationResult<()>, CliError> {
289    let contract_results = contract_packages
290        .par_iter()
291        .map(|package| -> Result<CompilationResult<()>, CliError> {
292            match compile_contract(file_manager, parsed_files, package, compile_options) {
293                Ok((contract, warnings)) => {
294                    let contract = nargo::ops::optimize_contract(contract);
295                    save_contract(
296                        contract,
297                        package,
298                        target_dir,
299                        compile_options.show_artifact_paths,
300                    )?;
301                    Ok(Ok(((), warnings)))
302                }
303                Err(errors_and_warnings) => Ok(Err(errors_and_warnings)),
304            }
305        })
306        .collect::<Result<Vec<_>, CliError>>()?;
307
308    // Collate any warnings/errors which were encountered during compilation.
309    let errors = collect_errors(contract_results).map(|(_, warnings)| ((), warnings));
310    Ok(errors)
311}
312
313fn save_contract(
314    contract: CompiledContract,
315    package: &Package,
316    target_dir: &Path,
317    show_artifact_paths: bool,
318) -> Result<(), CliError> {
319    let contract_name = contract.name.clone();
320    let artifact_path = save_contract_to_file(
321        &contract.into(),
322        &format!("{}-{}", package.name, contract_name),
323        target_dir,
324    )?;
325    if show_artifact_paths {
326        println!("Saved contract artifact to: {}", artifact_path.display());
327    }
328    Ok(())
329}