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::Mutex;
7
8use ::cairo_lang_diagnostics::ToOption;
9use anyhow::{Context, Result};
10use cairo_lang_defs::db::DefsGroup;
11use cairo_lang_filesystem::ids::{CrateId, CrateInput};
12use cairo_lang_lowering::db::LoweringGroup;
13use cairo_lang_lowering::ids::ConcreteFunctionWithBodyId;
14use cairo_lang_lowering::optimizations::config::Optimizations;
15use cairo_lang_lowering::utils::InliningStrategy;
16use cairo_lang_parser::db::ParserGroup;
17use cairo_lang_semantic::db::SemanticGroup;
18use cairo_lang_sierra::debug_info::{Annotations, DebugInfo};
19use cairo_lang_sierra::program::{Program, ProgramArtifact};
20use cairo_lang_sierra_generator::db::SierraGenGroup;
21use cairo_lang_sierra_generator::executables::{collect_executables, find_executable_function_ids};
22use cairo_lang_sierra_generator::program_generator::{
23    SierraProgramWithDebug, find_all_free_function_ids, try_get_function_with_body_id,
24};
25use cairo_lang_sierra_generator::replace_ids::replace_sierra_ids_in_program;
26use cairo_lang_utils::Intern;
27use cairo_lang_utils::unordered_hash_set::UnorderedHashSet;
28use salsa::{Database, join, par_map};
29
30use crate::db::RootDatabase;
31use crate::diagnostics::{DiagnosticsError, DiagnosticsReporter};
32use crate::project::{ProjectConfig, get_main_crate_ids_from_project, setup_project};
33
34pub mod db;
35pub mod diagnostics;
36pub mod project;
37
38#[cfg(test)]
39mod test;
40
41/// Configuration for the compiler.
42#[derive(Default)]
43pub struct CompilerConfig<'a> {
44    pub diagnostics_reporter: DiagnosticsReporter<'a>,
45
46    /// Replaces Sierra IDs with human-readable ones.
47    pub replace_ids: bool,
48
49    /// Adds a 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 a 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    inlining_strategy: InliningStrategy,
72) -> Result<Program> {
73    let mut db = RootDatabase::builder()
74        .with_optimizations(Optimizations::enabled_with_default_movable_functions(
75            inlining_strategy,
76        ))
77        .detect_corelib()
78        .build()?;
79    let main_crate_ids = setup_project(&mut db, path)?;
80    compile_prepared_db_program(
81        &db,
82        CrateInput::into_crate_ids(&db, main_crate_ids),
83        compiler_config,
84    )
85}
86
87/// Compiles a Cairo project.
88/// The project must be a valid Cairo project.
89/// This function is a wrapper over [`RootDatabase::builder()`] and [`compile_prepared_db_program`].
90/// # Arguments
91/// * `project_config` - The project configuration.
92/// * `compiler_config` - The compiler configuration.
93/// # Returns
94/// * `Ok(Program)` - The compiled program.
95/// * `Err(anyhow::Error)` - Compilation failed.
96pub fn compile(
97    project_config: ProjectConfig,
98    compiler_config: CompilerConfig<'_>,
99) -> Result<Program> {
100    let db = RootDatabase::builder()
101        .with_optimizations(Optimizations::enabled_with_default_movable_functions(
102            InliningStrategy::Default,
103        ))
104        .with_project_config(project_config.clone())
105        .build()?;
106    let main_crate_ids = get_main_crate_ids_from_project(&db, &project_config);
107
108    compile_prepared_db_program(&db, main_crate_ids, compiler_config)
109}
110
111/// Runs Cairo compiler.
112///
113/// # Arguments
114/// * `db` - Preloaded compilation database.
115/// * `main_crate_ids` - [`CrateId`]s to compile. Do not include dependencies here, only pass
116///   top-level crates in order to eliminate unused code. Use `CrateLongId::Real(name).intern(db)`
117///   in order to obtain [`CrateId`] from its name.
118/// * `compiler_config` - The compiler configuration.
119/// # Returns
120/// * `Ok(Program)` - The compiled program.
121/// * `Err(anyhow::Error)` - Compilation failed.
122pub fn compile_prepared_db_program<'db>(
123    db: &'db dyn Database,
124    main_crate_ids: Vec<CrateId<'db>>,
125    compiler_config: CompilerConfig<'_>,
126) -> Result<Program> {
127    Ok(compile_prepared_db(db, main_crate_ids, compiler_config)?.program)
128}
129
130/// Runs Cairo compiler.
131///
132/// Similar to `compile_prepared_db_program`, but this function returns all the raw debug
133/// information.
134///
135/// # Arguments
136/// * `db` - Preloaded compilation database.
137/// * `main_crate_ids` - [`CrateId`]s to compile. Do not include dependencies here, only pass
138///   top-level crates in order to eliminate unused code. Use `CrateLongId::Real(name).intern(db)`
139///   in order to obtain [`CrateId`] from its name.
140/// * `compiler_config` - The compiler configuration.
141/// # Returns
142/// * `Ok(SierraProgramWithDebug)` - The compiled program with debug info.
143/// * `Err(anyhow::Error)` - Compilation failed.
144pub fn compile_prepared_db<'db>(
145    db: &'db dyn Database,
146    main_crate_ids: Vec<CrateId<'db>>,
147    mut compiler_config: CompilerConfig<'_>,
148) -> Result<SierraProgramWithDebug<'db>> {
149    compiler_config.diagnostics_reporter.ensure(db)?;
150
151    let mut sierra_program_with_debug = db
152        .get_sierra_program(main_crate_ids)
153        .to_option()
154        .context("Compilation failed without any diagnostics")?
155        .clone();
156
157    if compiler_config.replace_ids {
158        sierra_program_with_debug.program =
159            replace_sierra_ids_in_program(db, &sierra_program_with_debug.program);
160    }
161
162    Ok(sierra_program_with_debug)
163}
164
165/// Checks if parallelism is available for warmup.
166fn should_warmup() -> bool {
167    rayon::current_num_threads() > 1
168}
169
170/// Checks if there are diagnostics and reports them to the provided callback as strings.
171/// Returns `Err` if diagnostics were found.
172///
173/// Note: Usually diagnostics should be checked as early as possible to avoid running into
174/// compilation errors that have not been reported to the user yet (which can result in compiler
175/// panic). This requires us to split the diagnostics warmup and function compilation warmup into
176/// two separate steps (note that we don't usually know the `ConcreteFunctionWithBodyId` yet when
177/// calculating diagnostics).
178///
179/// Performs parallel database warmup (if possible) and calls `DiagnosticsReporter::ensure`.
180pub fn ensure_diagnostics(
181    db: &dyn Database,
182    diagnostic_reporter: &mut DiagnosticsReporter<'_>,
183) -> std::result::Result<(), DiagnosticsError> {
184    if should_warmup() {
185        let crates = diagnostic_reporter.crates_of_interest(db);
186        join(db, |db| warmup_diagnostics_blocking(db, crates), |db| diagnostic_reporter.ensure(db))
187            .1
188    } else {
189        diagnostic_reporter.ensure(db)
190    }
191}
192
193/// Spawns threads to compute the diagnostics queries, making sure later calls for these queries
194/// would be faster as the queries were already computed.
195fn warmup_diagnostics_blocking(db: &dyn Database, crates: Vec<CrateInput>) {
196    let _: () = par_map(db, crates, |db, crate_input| {
197        let crate_id = crate_input.into_crate_long_id(db).intern(db);
198        let crate_modules = db.crate_modules(crate_id);
199        let _: () = par_map(db, crate_modules, |db, module_id| {
200            for file_id in db.module_files(*module_id).unwrap_or_default().iter().copied() {
201                db.file_syntax_diagnostics(file_id);
202            }
203            let _ = db.module_semantic_diagnostics(*module_id);
204            let _ = db.module_lowering_diagnostics(*module_id);
205        });
206    });
207}
208
209/// Spawns threads to compute the `function_with_body_sierra` query and all dependent queries for
210/// the requested functions and their dependencies.
211///
212/// Note that typically spawn_warmup_db should be used as this function is blocking.
213fn warmup_functions_blocking<'db>(
214    db: &dyn Database,
215    requested_function_ids: Vec<ConcreteFunctionWithBodyId<'db>>,
216) {
217    let processed_function_ids = &Mutex::new(UnorderedHashSet::<salsa::Id>::default());
218    let _: () = par_map(db, requested_function_ids, move |db, func_id| {
219        fn handle_func_inner<'db>(
220            processed_function_ids: &Mutex<UnorderedHashSet<salsa::Id>>,
221            snapshot: &dyn Database,
222            func_id: ConcreteFunctionWithBodyId<'db>,
223        ) {
224            if processed_function_ids.lock().unwrap().insert(func_id.as_intern_id()) {
225                let Ok(function) = snapshot.function_with_body_sierra(func_id) else {
226                    return;
227                };
228                let _: () = par_map(snapshot, &function.body, move |snapshot, statement| {
229                    let related_function_id: ConcreteFunctionWithBodyId<'_> =
230                        if let Some(r_id) = try_get_function_with_body_id(snapshot, statement) {
231                            r_id
232                        } else {
233                            return;
234                        };
235
236                    handle_func_inner(processed_function_ids, snapshot, related_function_id);
237                });
238            }
239        }
240        handle_func_inner(processed_function_ids, db, func_id)
241    });
242}
243
244///  Checks if there are diagnostics in the database and if there are None, returns
245///  the [SierraProgramWithDebug] object of the requested functions
246pub fn get_sierra_program_for_functions<'db>(
247    db: &'db dyn Database,
248    requested_function_ids: Vec<ConcreteFunctionWithBodyId<'db>>,
249) -> Result<&'db SierraProgramWithDebug<'db>> {
250    if should_warmup() {
251        let requested_function_ids = requested_function_ids.clone();
252        warmup_functions_blocking(db, requested_function_ids);
253    }
254    db.get_sierra_program_for_functions(requested_function_ids)
255        .to_option()
256        .context("Compilation failed without any diagnostics.")
257}
258
259/// Runs Cairo compiler for specified crates.
260///
261/// Wrapper over [`compile_prepared_db`], but this function returns [`ProgramArtifact`]
262/// with requested debug info.
263///
264/// # Arguments
265/// * `db` - Preloaded compilation database.
266/// * `main_crate_ids` - [`CrateId`]s to compile. Do not include dependencies here, only pass
267///   top-level crates in order to eliminate unused code. Use `CrateLongId::Real(name).intern(db)`
268///   in order to obtain [`CrateId`] from its name.
269/// * `compiler_config` - The compiler configuration.
270/// # Returns
271/// * `Ok(ProgramArtifact)` - The compiled program artifact with requested debug info.
272/// * `Err(anyhow::Error)` - Compilation failed.
273pub fn compile_prepared_db_program_artifact<'db>(
274    db: &'db dyn Database,
275    main_crate_ids: Vec<CrateId<'db>>,
276    mut compiler_config: CompilerConfig<'_>,
277) -> Result<ProgramArtifact> {
278    ensure_diagnostics(db, &mut compiler_config.diagnostics_reporter)?;
279
280    let executable_functions = find_executable_function_ids(db, main_crate_ids.clone());
281
282    let function_ids = if executable_functions.is_empty() {
283        // No executables found - compile for all main crates.
284        // TODO(maciektr): Deprecate in future. This compilation is useless, without `replace_ids`.
285        find_all_free_function_ids(db, main_crate_ids)
286            .to_option()
287            .context("Compilation failed without any diagnostics.")?
288    } else {
289        // Compile for executable functions only.
290        executable_functions.clone().into_keys().collect()
291    };
292
293    let mut program_artifact =
294        compile_prepared_db_program_artifact_for_functions(db, function_ids, compiler_config)?;
295
296    // Calculate executable function Sierra ids.
297    let executables = collect_executables(db, executable_functions, &program_artifact.program);
298
299    let debug_info = program_artifact.debug_info.take().unwrap_or_default();
300
301    Ok(program_artifact.with_debug_info(DebugInfo { executables, ..debug_info }))
302}
303
304/// Runs Cairo compiler for specified functions.
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/// * `requested_function_ids` - [`ConcreteFunctionWithBodyId`]s to compile.
312/// * `compiler_config` - The compiler configuration.
313/// # Returns
314/// * `Ok(ProgramArtifact)` - The compiled program artifact with requested debug info.
315/// * `Err(anyhow::Error)` - Compilation failed.
316pub fn compile_prepared_db_program_artifact_for_functions<'db>(
317    db: &'db dyn Database,
318    requested_function_ids: Vec<ConcreteFunctionWithBodyId<'db>>,
319    compiler_config: CompilerConfig<'_>,
320) -> Result<ProgramArtifact> {
321    let mut sierra_program_with_debug =
322        get_sierra_program_for_functions(db, requested_function_ids)?.clone();
323
324    if compiler_config.replace_ids {
325        sierra_program_with_debug.program =
326            replace_sierra_ids_in_program(db, &sierra_program_with_debug.program);
327    }
328
329    let mut annotations = Annotations::default();
330
331    if compiler_config.add_statements_functions {
332        annotations.extend(Annotations::from(
333            sierra_program_with_debug
334                .debug_info
335                .statements_locations
336                .extract_statements_functions(db),
337        ))
338    };
339
340    if compiler_config.add_statements_code_locations {
341        annotations.extend(Annotations::from(
342            sierra_program_with_debug
343                .debug_info
344                .statements_locations
345                .extract_statements_source_code_locations(db),
346        ))
347    };
348
349    let debug_info = DebugInfo {
350        type_names: Default::default(),
351        libfunc_names: Default::default(),
352        user_func_names: Default::default(),
353        annotations,
354        executables: Default::default(),
355    };
356
357    Ok(ProgramArtifact::stripped(sierra_program_with_debug.program).with_debug_info(debug_info))
358}