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