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