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