cairo_lang_compiler/
lib.rs

1//! Cairo compiler.
2//!
3//! This crate is responsible for compiling a Cairo project into a Sierra program.
4//! It is the main entry point for the compiler.
5use std::path::Path;
6use std::sync::{Arc, Mutex};
7
8use ::cairo_lang_diagnostics::ToOption;
9use anyhow::{Context, Result};
10use cairo_lang_filesystem::ids::CrateId;
11use cairo_lang_lowering::ids::ConcreteFunctionWithBodyId;
12use cairo_lang_lowering::utils::InliningStrategy;
13use cairo_lang_sierra::debug_info::{Annotations, DebugInfo};
14use cairo_lang_sierra::program::{Program, ProgramArtifact};
15use cairo_lang_sierra_generator::db::SierraGenGroup;
16use cairo_lang_sierra_generator::executables::{collect_executables, find_executable_function_ids};
17use cairo_lang_sierra_generator::program_generator::{
18    SierraProgramWithDebug, try_get_function_with_body_id,
19};
20use cairo_lang_sierra_generator::replace_ids::replace_sierra_ids_in_program;
21use cairo_lang_utils::unordered_hash_set::UnorderedHashSet;
22
23use crate::db::RootDatabase;
24use crate::diagnostics::DiagnosticsReporter;
25use crate::project::{ProjectConfig, get_main_crate_ids_from_project, setup_project};
26
27pub mod db;
28pub mod diagnostics;
29pub mod project;
30
31#[cfg(test)]
32mod test;
33
34/// Configuration for the compiler.
35#[derive(Default)]
36pub struct CompilerConfig<'c> {
37    pub diagnostics_reporter: DiagnosticsReporter<'c>,
38
39    /// Replaces sierra ids with human-readable ones.
40    pub replace_ids: bool,
41
42    /// Disables inlining functions.
43    pub inlining_strategy: InliningStrategy,
44
45    /// The name of the allowed libfuncs list to use in compilation.
46    /// If None the default list of audited libfuncs will be used.
47    pub allowed_libfuncs_list_name: Option<String>,
48
49    /// Adds mapping used by [cairo-profiler](https://github.com/software-mansion/cairo-profiler) to
50    /// [cairo_lang_sierra::debug_info::Annotations] in [cairo_lang_sierra::debug_info::DebugInfo].
51    pub add_statements_functions: bool,
52
53    /// Adds mapping used by [cairo-coverage](https://github.com/software-mansion/cairo-coverage) to
54    /// [cairo_lang_sierra::debug_info::Annotations] in [cairo_lang_sierra::debug_info::DebugInfo].
55    pub add_statements_code_locations: bool,
56}
57
58/// Compiles a Cairo project at the given path.
59/// The project must be a valid Cairo project:
60/// Either a standalone `.cairo` file (a single crate), or a directory with a `cairo_project.toml`
61/// file.
62/// # Arguments
63/// * `path` - The path to the project.
64/// * `compiler_config` - The compiler configuration.
65/// # Returns
66/// * `Ok(Program)` - The compiled program.
67/// * `Err(anyhow::Error)` - Compilation failed.
68pub fn compile_cairo_project_at_path(
69    path: &Path,
70    compiler_config: CompilerConfig<'_>,
71) -> Result<Program> {
72    let mut db = RootDatabase::builder()
73        .with_inlining_strategy(compiler_config.inlining_strategy)
74        .detect_corelib()
75        .build()?;
76    let main_crate_ids = setup_project(&mut db, path)?;
77    compile_prepared_db_program(&mut db, main_crate_ids, compiler_config)
78}
79
80/// Compiles a Cairo project.
81/// The project must be a valid Cairo project.
82/// This function is a wrapper over [`RootDatabase::builder()`] and [`compile_prepared_db_program`].
83/// # Arguments
84/// * `project_config` - The project configuration.
85/// * `compiler_config` - The compiler configuration.
86/// # Returns
87/// * `Ok(Program)` - The compiled program.
88/// * `Err(anyhow::Error)` - Compilation failed.
89pub fn compile(
90    project_config: ProjectConfig,
91    compiler_config: CompilerConfig<'_>,
92) -> Result<Program> {
93    let mut db = RootDatabase::builder()
94        .with_inlining_strategy(compiler_config.inlining_strategy)
95        .with_project_config(project_config.clone())
96        .build()?;
97    let main_crate_ids = get_main_crate_ids_from_project(&mut db, &project_config);
98
99    compile_prepared_db_program(&mut db, main_crate_ids, compiler_config)
100}
101
102/// Runs Cairo compiler.
103///
104/// # Arguments
105/// * `db` - Preloaded compilation database.
106/// * `main_crate_ids` - [`CrateId`]s to compile. Do not include dependencies here, only pass
107///   top-level crates in order to eliminate unused code. Use `CrateLongId::Real(name).intern(db)`
108///   in order to obtain [`CrateId`] from its name.
109/// * `compiler_config` - The compiler configuration.
110/// # Returns
111/// * `Ok(Program)` - The compiled program.
112/// * `Err(anyhow::Error)` - Compilation failed.
113pub fn compile_prepared_db_program(
114    db: &mut RootDatabase,
115    main_crate_ids: Vec<CrateId>,
116    compiler_config: CompilerConfig<'_>,
117) -> Result<Program> {
118    Ok(compile_prepared_db(db, main_crate_ids, compiler_config)?.program)
119}
120
121/// Runs Cairo compiler.
122///
123/// Similar to `compile_prepared_db_program`, but this function returns all the raw debug
124/// information.
125///
126/// # Arguments
127/// * `db` - Preloaded compilation database.
128/// * `main_crate_ids` - [`CrateId`]s to compile. Do not include dependencies here, only pass
129///   top-level crates in order to eliminate unused code. Use `CrateLongId::Real(name).intern(db)`
130///   in order to obtain [`CrateId`] from its name.
131/// * `compiler_config` - The compiler configuration.
132/// # Returns
133/// * `Ok(SierraProgramWithDebug)` - The compiled program with debug info.
134/// * `Err(anyhow::Error)` - Compilation failed.
135pub fn compile_prepared_db(
136    db: &RootDatabase,
137    main_crate_ids: Vec<CrateId>,
138    mut compiler_config: CompilerConfig<'_>,
139) -> Result<SierraProgramWithDebug> {
140    compiler_config.diagnostics_reporter.ensure(db)?;
141
142    let mut sierra_program_with_debug = Arc::unwrap_or_clone(
143        db.get_sierra_program(main_crate_ids)
144            .to_option()
145            .context("Compilation failed without any diagnostics")?,
146    );
147
148    if compiler_config.replace_ids {
149        sierra_program_with_debug.program =
150            replace_sierra_ids_in_program(db, &sierra_program_with_debug.program);
151    }
152
153    Ok(sierra_program_with_debug)
154}
155
156/// Spawns threads to compute the `function_with_body_sierra` query and all dependent queries for
157/// the requested functions and their dependencies.
158///
159/// Note that typically spawn_warmup_db should be used as this function is blocking.
160fn warmup_db_blocking(
161    snapshot: salsa::Snapshot<RootDatabase>,
162    requested_function_ids: Vec<ConcreteFunctionWithBodyId>,
163) {
164    let processed_function_ids =
165        &Mutex::new(UnorderedHashSet::<ConcreteFunctionWithBodyId>::default());
166    rayon::scope(move |s| {
167        for func_id in requested_function_ids {
168            let snapshot = salsa::ParallelDatabase::snapshot(&*snapshot);
169
170            s.spawn(move |_| {
171                fn handle_func_inner(
172                    processed_function_ids: &Mutex<UnorderedHashSet<ConcreteFunctionWithBodyId>>,
173                    snapshot: salsa::Snapshot<RootDatabase>,
174                    func_id: ConcreteFunctionWithBodyId,
175                ) {
176                    if processed_function_ids.lock().unwrap().insert(func_id) {
177                        rayon::scope(move |s| {
178                            let db = &*snapshot;
179                            let Ok(function) = db.function_with_body_sierra(func_id) else {
180                                return;
181                            };
182                            for statement in &function.body {
183                                let Some(related_function_id) =
184                                    try_get_function_with_body_id(db, statement)
185                                else {
186                                    continue;
187                                };
188
189                                let snapshot = salsa::ParallelDatabase::snapshot(&*snapshot);
190                                s.spawn(move |_| {
191                                    handle_func_inner(
192                                        processed_function_ids,
193                                        snapshot,
194                                        related_function_id,
195                                    )
196                                })
197                            }
198                        });
199                    }
200                }
201
202                handle_func_inner(processed_function_ids, snapshot, func_id)
203            });
204        }
205    });
206}
207
208/// Spawns a task to warm up the db.
209fn spawn_warmup_db(db: &RootDatabase, requested_function_ids: Vec<ConcreteFunctionWithBodyId>) {
210    let snapshot = salsa::ParallelDatabase::snapshot(db);
211    rayon::spawn(move || warmup_db_blocking(snapshot, requested_function_ids));
212}
213
214///  Checks if there are diagnostics in the database and if there are None, returns
215///  the [SierraProgramWithDebug] object of the requested functions
216pub fn get_sierra_program_for_functions(
217    db: &RootDatabase,
218    requested_function_ids: Vec<ConcreteFunctionWithBodyId>,
219    mut diagnostic_reporter: DiagnosticsReporter<'_>,
220) -> Result<Arc<SierraProgramWithDebug>> {
221    if rayon::current_num_threads() > 1 {
222        // If we have more than one thread, we can use the other threads to warm up the db.
223        diagnostic_reporter.warm_up_diagnostics(db);
224        spawn_warmup_db(db, requested_function_ids.clone());
225    }
226
227    diagnostic_reporter.ensure(db)?;
228    db.get_sierra_program_for_functions(requested_function_ids)
229        .to_option()
230        .with_context(|| "Compilation failed without any diagnostics.")
231}
232
233/// Runs Cairo compiler.
234///
235/// Wrapper over [`compile_prepared_db`], but this function returns [`ProgramArtifact`]
236/// with requested debug info.
237///
238/// # Arguments
239/// * `db` - Preloaded compilation database.
240/// * `main_crate_ids` - [`CrateId`]s to compile. Do not include dependencies here, only pass
241///   top-level crates in order to eliminate unused code. Use `CrateLongId::Real(name).intern(db)`
242///   in order to obtain [`CrateId`] from its name.
243/// * `compiler_config` - The compiler configuration.
244/// # Returns
245/// * `Ok(ProgramArtifact)` - The compiled program artifact with requested debug info.
246/// * `Err(anyhow::Error)` - Compilation failed.
247pub fn compile_prepared_db_program_artifact(
248    db: &mut RootDatabase,
249    main_crate_ids: Vec<CrateId>,
250    mut compiler_config: CompilerConfig<'_>,
251) -> Result<ProgramArtifact> {
252    let add_statements_functions = compiler_config.add_statements_functions;
253    let add_statements_code_locations = compiler_config.add_statements_code_locations;
254
255    compiler_config.diagnostics_reporter.ensure(db)?;
256
257    let executable_functions = find_executable_function_ids(db, main_crate_ids.clone());
258
259    let mut sierra_program_with_debug = if executable_functions.is_empty() {
260        // No executables found - compile for all main crates.
261        // TODO(maciektr): Deprecate in future. This compilation is useless, without `replace_ids`.
262        Arc::unwrap_or_clone(
263            db.get_sierra_program(main_crate_ids)
264                .to_option()
265                .context("Compilation failed without any diagnostics")?,
266        )
267    } else {
268        // Compile for executable functions only.
269        Arc::unwrap_or_clone(
270            db.get_sierra_program_for_functions(executable_functions.clone().into_keys().collect())
271                .to_option()
272                .context("Compilation failed without any diagnostics")?,
273        )
274    };
275
276    if compiler_config.replace_ids {
277        sierra_program_with_debug.program =
278            replace_sierra_ids_in_program(db, &sierra_program_with_debug.program);
279    }
280
281    let mut annotations = Annotations::default();
282
283    if add_statements_functions {
284        annotations.extend(Annotations::from(
285            sierra_program_with_debug
286                .debug_info
287                .statements_locations
288                .extract_statements_functions(db),
289        ))
290    };
291
292    if add_statements_code_locations {
293        annotations.extend(Annotations::from(
294            sierra_program_with_debug
295                .debug_info
296                .statements_locations
297                .extract_statements_source_code_locations(db),
298        ))
299    };
300
301    let debug_info = DebugInfo {
302        type_names: Default::default(),
303        libfunc_names: Default::default(),
304        user_func_names: Default::default(),
305        annotations,
306        executables: Default::default(),
307    };
308
309    // Calculate executable function Sierra ids.
310    let executables =
311        collect_executables(db, executable_functions, &sierra_program_with_debug.program);
312
313    Ok(ProgramArtifact::stripped(sierra_program_with_debug.program)
314        .with_debug_info(DebugInfo { executables, ..debug_info }))
315}