Skip to main content

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