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;
22use rayon::{ThreadPool, ThreadPoolBuilder};
23
24use crate::db::RootDatabase;
25use crate::diagnostics::{DiagnosticsError, DiagnosticsReporter};
26use crate::project::{ProjectConfig, get_main_crate_ids_from_project, setup_project};
27
28pub mod db;
29pub mod diagnostics;
30pub mod project;
31
32#[cfg(test)]
33mod test;
34
35/// Configuration for the compiler.
36#[derive(Default)]
37pub struct CompilerConfig<'c> {
38    pub diagnostics_reporter: DiagnosticsReporter<'c>,
39
40    /// Replaces sierra ids with human-readable ones.
41    pub replace_ids: bool,
42
43    /// Disables inlining functions.
44    pub inlining_strategy: InliningStrategy,
45
46    /// The name of the allowed libfuncs list to use in compilation.
47    /// If None the default list of audited libfuncs will be used.
48    pub allowed_libfuncs_list_name: Option<String>,
49
50    /// Adds mapping used by [cairo-profiler](https://github.com/software-mansion/cairo-profiler) to
51    /// [cairo_lang_sierra::debug_info::Annotations] in [cairo_lang_sierra::debug_info::DebugInfo].
52    pub add_statements_functions: bool,
53
54    /// Adds mapping used by [cairo-coverage](https://github.com/software-mansion/cairo-coverage) to
55    /// [cairo_lang_sierra::debug_info::Annotations] in [cairo_lang_sierra::debug_info::DebugInfo].
56    pub add_statements_code_locations: bool,
57}
58
59/// Compiles a Cairo project at the given path.
60/// The project must be a valid Cairo project:
61/// Either a standalone `.cairo` file (a single crate), or a directory with a `cairo_project.toml`
62/// file.
63/// # Arguments
64/// * `path` - The path to the project.
65/// * `compiler_config` - The compiler configuration.
66/// # Returns
67/// * `Ok(Program)` - The compiled program.
68/// * `Err(anyhow::Error)` - Compilation failed.
69pub fn compile_cairo_project_at_path(
70    path: &Path,
71    compiler_config: CompilerConfig<'_>,
72) -> Result<Program> {
73    let mut db = RootDatabase::builder()
74        .with_inlining_strategy(compiler_config.inlining_strategy)
75        .detect_corelib()
76        .build()?;
77    let main_crate_ids = setup_project(&mut db, path)?;
78    compile_prepared_db_program(&mut db, main_crate_ids, compiler_config)
79}
80
81/// Compiles a Cairo project.
82/// The project must be a valid Cairo project.
83/// This function is a wrapper over [`RootDatabase::builder()`] and [`compile_prepared_db_program`].
84/// # Arguments
85/// * `project_config` - The project configuration.
86/// * `compiler_config` - The compiler configuration.
87/// # Returns
88/// * `Ok(Program)` - The compiled program.
89/// * `Err(anyhow::Error)` - Compilation failed.
90pub fn compile(
91    project_config: ProjectConfig,
92    compiler_config: CompilerConfig<'_>,
93) -> Result<Program> {
94    let mut db = RootDatabase::builder()
95        .with_inlining_strategy(compiler_config.inlining_strategy)
96        .with_project_config(project_config.clone())
97        .build()?;
98    let main_crate_ids = get_main_crate_ids_from_project(&mut db, &project_config);
99
100    compile_prepared_db_program(&mut db, main_crate_ids, compiler_config)
101}
102
103/// Runs Cairo compiler.
104///
105/// # Arguments
106/// * `db` - Preloaded compilation database.
107/// * `main_crate_ids` - [`CrateId`]s to compile. Do not include dependencies here, only pass
108///   top-level crates in order to eliminate unused code. Use `CrateLongId::Real(name).intern(db)`
109///   in order to obtain [`CrateId`] from its name.
110/// * `compiler_config` - The compiler configuration.
111/// # Returns
112/// * `Ok(Program)` - The compiled program.
113/// * `Err(anyhow::Error)` - Compilation failed.
114pub fn compile_prepared_db_program(
115    db: &mut RootDatabase,
116    main_crate_ids: Vec<CrateId>,
117    compiler_config: CompilerConfig<'_>,
118) -> Result<Program> {
119    Ok(compile_prepared_db(db, main_crate_ids, compiler_config)?.program)
120}
121
122/// Runs Cairo compiler.
123///
124/// Similar to `compile_prepared_db_program`, but this function returns all the raw debug
125/// information.
126///
127/// # Arguments
128/// * `db` - Preloaded compilation database.
129/// * `main_crate_ids` - [`CrateId`]s to compile. Do not include dependencies here, only pass
130///   top-level crates in order to eliminate unused code. Use `CrateLongId::Real(name).intern(db)`
131///   in order to obtain [`CrateId`] from its name.
132/// * `compiler_config` - The compiler configuration.
133/// # Returns
134/// * `Ok(SierraProgramWithDebug)` - The compiled program with debug info.
135/// * `Err(anyhow::Error)` - Compilation failed.
136pub fn compile_prepared_db(
137    db: &RootDatabase,
138    main_crate_ids: Vec<CrateId>,
139    mut compiler_config: CompilerConfig<'_>,
140) -> Result<SierraProgramWithDebug> {
141    compiler_config.diagnostics_reporter.ensure(db)?;
142
143    let mut sierra_program_with_debug = Arc::unwrap_or_clone(
144        db.get_sierra_program(main_crate_ids)
145            .to_option()
146            .context("Compilation failed without any diagnostics")?,
147    );
148
149    if compiler_config.replace_ids {
150        sierra_program_with_debug.program =
151            replace_sierra_ids_in_program(db, &sierra_program_with_debug.program);
152    }
153
154    Ok(sierra_program_with_debug)
155}
156
157/// Context for database warmup.
158///
159/// This struct will spawn a thread pool that can be used for parallel database warmup.
160/// This can be both diagnostics warmup and function compilation warmup.
161/// We encapsulate the thread pool here so that we can reuse it easily for both.
162/// Note: Usually diagnostics should be checked as early as possible to avoid running into
163/// compilation errors that have not been reported to the user yet (which can result in compiler
164/// panic). This requires us to split the diagnostics warmup and function compilation warmup into
165/// two separate steps (note that we don't usually know the `ConcreteFunctionWithBodyId` yet when
166/// calculating diagnostics).
167pub enum DbWarmupContext {
168    Warmup { pool: ThreadPool },
169    NoWarmup,
170}
171
172impl DbWarmupContext {
173    /// Creates a new thread pool.
174    pub fn new() -> Self {
175        if !Self::should_warmup() {
176            return Self::NoWarmup;
177        }
178        const MAX_WARMUP_PARALLELISM: usize = 4;
179        let pool = ThreadPoolBuilder::new()
180            .num_threads(rayon::current_num_threads().min(MAX_WARMUP_PARALLELISM))
181            .build()
182            .expect("failed to build rayon thread pool");
183        Self::Warmup { pool }
184    }
185
186    /// Checks if parallelism is available for warmup.
187    fn should_warmup() -> bool {
188        rayon::current_num_threads() > 1
189    }
190
191    /// Performs parallel database warmup (if possible)
192    fn warmup_diagnostics(
193        &self,
194        db: &RootDatabase,
195        diagnostic_reporter: &mut DiagnosticsReporter<'_>,
196    ) {
197        match self {
198            Self::Warmup { pool } => diagnostic_reporter.warm_up_diagnostics(db, pool),
199            Self::NoWarmup => {}
200        }
201    }
202
203    /// Checks if there are diagnostics and reports them to the provided callback as strings.
204    /// Returns `Err` if diagnostics were found.
205    ///
206    /// Performs parallel database warmup (if possible) and calls `DiagnosticsReporter::ensure`.
207    pub fn ensure_diagnostics(
208        &self,
209        db: &RootDatabase,
210        diagnostic_reporter: &mut DiagnosticsReporter<'_>,
211    ) -> std::result::Result<(), DiagnosticsError> {
212        self.warmup_diagnostics(db, diagnostic_reporter);
213        diagnostic_reporter.ensure(db)?;
214        Ok(())
215    }
216
217    /// Spawns a task to warm up the db for the requested functions (if possible).
218    fn warmup_db(
219        &self,
220        db: &RootDatabase,
221        requested_function_ids: Vec<ConcreteFunctionWithBodyId>,
222    ) {
223        match self {
224            Self::Warmup { pool } => {
225                let snapshot = salsa::ParallelDatabase::snapshot(db);
226                pool.spawn(move || warmup_db_blocking(snapshot, requested_function_ids));
227            }
228            Self::NoWarmup => {}
229        }
230    }
231}
232
233impl Default for DbWarmupContext {
234    fn default() -> Self {
235        Self::new()
236    }
237}
238
239/// Spawns threads to compute the `function_with_body_sierra` query and all dependent queries for
240/// the requested functions and their dependencies.
241///
242/// Note that typically spawn_warmup_db should be used as this function is blocking.
243fn warmup_db_blocking(
244    snapshot: salsa::Snapshot<RootDatabase>,
245    requested_function_ids: Vec<ConcreteFunctionWithBodyId>,
246) {
247    let processed_function_ids =
248        &Mutex::new(UnorderedHashSet::<ConcreteFunctionWithBodyId>::default());
249    rayon::scope(move |s| {
250        for func_id in requested_function_ids {
251            let snapshot = salsa::ParallelDatabase::snapshot(&*snapshot);
252
253            s.spawn(move |_| {
254                fn handle_func_inner(
255                    processed_function_ids: &Mutex<UnorderedHashSet<ConcreteFunctionWithBodyId>>,
256                    snapshot: salsa::Snapshot<RootDatabase>,
257                    func_id: ConcreteFunctionWithBodyId,
258                ) {
259                    if processed_function_ids.lock().unwrap().insert(func_id) {
260                        rayon::scope(move |s| {
261                            let db = &*snapshot;
262                            let Ok(function) = db.function_with_body_sierra(func_id) else {
263                                return;
264                            };
265                            for statement in &function.body {
266                                let Some(related_function_id) =
267                                    try_get_function_with_body_id(db, statement)
268                                else {
269                                    continue;
270                                };
271
272                                let snapshot = salsa::ParallelDatabase::snapshot(&*snapshot);
273                                s.spawn(move |_| {
274                                    handle_func_inner(
275                                        processed_function_ids,
276                                        snapshot,
277                                        related_function_id,
278                                    )
279                                })
280                            }
281                        });
282                    }
283                }
284
285                handle_func_inner(processed_function_ids, snapshot, func_id)
286            });
287        }
288    });
289}
290
291///  Checks if there are diagnostics in the database and if there are None, returns
292///  the [SierraProgramWithDebug] object of the requested functions
293pub fn get_sierra_program_for_functions(
294    db: &RootDatabase,
295    requested_function_ids: Vec<ConcreteFunctionWithBodyId>,
296    context: DbWarmupContext,
297) -> Result<Arc<SierraProgramWithDebug>> {
298    context.warmup_db(db, requested_function_ids.clone());
299    db.get_sierra_program_for_functions(requested_function_ids)
300        .to_option()
301        .with_context(|| "Compilation failed without any diagnostics.")
302}
303
304/// Runs Cairo compiler.
305///
306/// Wrapper over [`compile_prepared_db`], but this function returns [`ProgramArtifact`]
307/// with requested debug info.
308///
309/// # Arguments
310/// * `db` - Preloaded compilation database.
311/// * `main_crate_ids` - [`CrateId`]s to compile. Do not include dependencies here, only pass
312///   top-level crates in order to eliminate unused code. Use `CrateLongId::Real(name).intern(db)`
313///   in order to obtain [`CrateId`] from its name.
314/// * `compiler_config` - The compiler configuration.
315/// # Returns
316/// * `Ok(ProgramArtifact)` - The compiled program artifact with requested debug info.
317/// * `Err(anyhow::Error)` - Compilation failed.
318pub fn compile_prepared_db_program_artifact(
319    db: &mut RootDatabase,
320    main_crate_ids: Vec<CrateId>,
321    mut compiler_config: CompilerConfig<'_>,
322) -> Result<ProgramArtifact> {
323    let add_statements_functions = compiler_config.add_statements_functions;
324    let add_statements_code_locations = compiler_config.add_statements_code_locations;
325
326    compiler_config.diagnostics_reporter.ensure(db)?;
327
328    let executable_functions = find_executable_function_ids(db, main_crate_ids.clone());
329
330    let mut sierra_program_with_debug = if executable_functions.is_empty() {
331        // No executables found - compile for all main crates.
332        // TODO(maciektr): Deprecate in future. This compilation is useless, without `replace_ids`.
333        Arc::unwrap_or_clone(
334            db.get_sierra_program(main_crate_ids)
335                .to_option()
336                .context("Compilation failed without any diagnostics")?,
337        )
338    } else {
339        // Compile for executable functions only.
340        Arc::unwrap_or_clone(
341            db.get_sierra_program_for_functions(executable_functions.clone().into_keys().collect())
342                .to_option()
343                .context("Compilation failed without any diagnostics")?,
344        )
345    };
346
347    if compiler_config.replace_ids {
348        sierra_program_with_debug.program =
349            replace_sierra_ids_in_program(db, &sierra_program_with_debug.program);
350    }
351
352    let mut annotations = Annotations::default();
353
354    if add_statements_functions {
355        annotations.extend(Annotations::from(
356            sierra_program_with_debug
357                .debug_info
358                .statements_locations
359                .extract_statements_functions(db),
360        ))
361    };
362
363    if add_statements_code_locations {
364        annotations.extend(Annotations::from(
365            sierra_program_with_debug
366                .debug_info
367                .statements_locations
368                .extract_statements_source_code_locations(db),
369        ))
370    };
371
372    let debug_info = DebugInfo {
373        type_names: Default::default(),
374        libfunc_names: Default::default(),
375        user_func_names: Default::default(),
376        annotations,
377        executables: Default::default(),
378    };
379
380    // Calculate executable function Sierra ids.
381    let executables =
382        collect_executables(db, executable_functions, &sierra_program_with_debug.program);
383
384    Ok(ProgramArtifact::stripped(sierra_program_with_debug.program)
385        .with_debug_info(DebugInfo { executables, ..debug_info }))
386}