cairo_lang_starknet/
compile.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result};
4use cairo_lang_compiler::db::RootDatabase;
5use cairo_lang_compiler::project::setup_project;
6use cairo_lang_compiler::{CompilerConfig, ensure_diagnostics};
7use cairo_lang_defs::ids::TopLevelLanguageElementId;
8use cairo_lang_diagnostics::ToOption;
9use cairo_lang_filesystem::ids::{CrateId, CrateInput};
10use cairo_lang_lowering::ids::ConcreteFunctionWithBodyId;
11use cairo_lang_lowering::optimizations::config::Optimizations;
12use cairo_lang_lowering::utils::InliningStrategy;
13use cairo_lang_sierra::debug_info::Annotations;
14use cairo_lang_sierra_generator::canonical_id_replacer::CanonicalReplacer;
15use cairo_lang_sierra_generator::db::SierraGenGroup;
16use cairo_lang_sierra_generator::program_generator::SierraProgramWithDebug;
17use cairo_lang_sierra_generator::replace_ids::{SierraIdReplacer, replace_sierra_ids_in_program};
18use cairo_lang_starknet_classes::allowed_libfuncs::ListSelector;
19use cairo_lang_starknet_classes::contract_class::{
20    ContractClass, ContractEntryPoint, ContractEntryPoints,
21};
22use itertools::{Itertools, chain};
23use salsa::{Database, par_map};
24
25use crate::abi::AbiBuilder;
26use crate::aliased::Aliased;
27use crate::contract::{
28    ContractDeclaration, find_contracts, get_contract_abi_functions,
29    get_selector_and_sierra_function,
30};
31use crate::plugin::consts::{CONSTRUCTOR_MODULE, EXTERNAL_MODULE, L1_HANDLER_MODULE};
32use crate::starknet_plugin_suite;
33
34#[cfg(test)]
35#[path = "compile_test.rs"]
36mod test;
37
38/// Compile the contract given by path.
39/// Errors if there is ambiguity.
40pub fn compile_path(
41    path: &Path,
42    contract_path: Option<&str>,
43    mut compiler_config: CompilerConfig<'_>,
44    inlining_strategy: InliningStrategy,
45) -> Result<ContractClass> {
46    let mut db = RootDatabase::builder()
47        .with_optimizations(Optimizations::enabled_with_default_movable_functions(
48            inlining_strategy,
49        ))
50        .detect_corelib()
51        .with_default_plugin_suite(starknet_plugin_suite())
52        .build()?;
53
54    let main_crate_inputs = setup_project(&mut db, Path::new(&path))?;
55    compiler_config.diagnostics_reporter =
56        compiler_config.diagnostics_reporter.with_crates(&main_crate_inputs);
57    let main_crate_ids = CrateInput::into_crate_ids(&db, main_crate_inputs);
58    compile_contract_in_prepared_db(&db, contract_path, main_crate_ids, compiler_config)
59}
60
61/// Runs Starknet contract compiler on the specified contract.
62/// If no contract was specified, verify that there is only one.
63/// Otherwise, return an error.
64pub fn compile_contract_in_prepared_db<'db>(
65    db: &'db dyn Database,
66    contract_path: Option<&str>,
67    main_crate_ids: Vec<CrateId<'db>>,
68    mut compiler_config: CompilerConfig<'_>,
69) -> Result<ContractClass> {
70    let mut contracts = find_contracts(db, &main_crate_ids);
71
72    // TODO(ilya): Add contract names.
73    if let Some(contract_path) = contract_path {
74        contracts.retain(|contract| contract.submodule_id.full_path(db) == contract_path);
75    };
76    let contract = match contracts.len() {
77        0 => {
78            // Report diagnostics as they might reveal the reason why no contract was found.
79            compiler_config.diagnostics_reporter.ensure(db)?;
80            anyhow::bail!("Contract not found.");
81        }
82        1 => &contracts[0],
83        _ => {
84            let contract_names =
85                contracts.iter().map(|contract| contract.submodule_id.full_path(db)).join("\n  ");
86            anyhow::bail!(
87                "More than one contract found in the main crate: \n  {}\nUse --contract-path to \
88                 specify which to compile.",
89                contract_names
90            );
91        }
92    };
93
94    let contracts = vec![contract];
95    let mut classes = compile_prepared_db(db, &contracts, compiler_config)?;
96    assert_eq!(classes.len(), 1);
97    Ok(classes.remove(0))
98}
99
100/// Runs Starknet contracts compiler.
101///
102/// # Arguments
103/// * `db` - Preloaded compilation database.
104/// * `contracts` - [`ContractDeclaration`]s to compile. Use [`find_contracts`] to find contracts in
105///   `db`.
106/// * `compiler_config` - The compiler configuration.
107/// # Returns
108/// * `Ok(Vec<ContractClass>)` - List of all compiled contract classes found in main crates.
109/// * `Err(anyhow::Error)` - Compilation failed.
110pub fn compile_prepared_db<'db>(
111    db: &'db dyn Database,
112    contracts: &[&ContractDeclaration<'db>],
113    mut compiler_config: CompilerConfig<'_>,
114) -> Result<Vec<ContractClass>> {
115    ensure_diagnostics(db, &mut compiler_config.diagnostics_reporter)?;
116
117    par_map(db, contracts, |db, contract| {
118        compile_contract_with_prepared_and_checked_db(db, contract, &compiler_config)
119    })
120}
121
122/// Compile declared Starknet contract.
123///
124/// The `contract` value **must** come from `db`, for example as a result of calling
125/// [`find_contracts`]. Does not check diagnostics, it is expected that they are checked by caller
126/// of this function.
127fn compile_contract_with_prepared_and_checked_db<'db>(
128    db: &'db dyn Database,
129    contract: &ContractDeclaration<'db>,
130    compiler_config: &CompilerConfig<'_>,
131) -> Result<ContractClass> {
132    let SemanticEntryPoints { external, l1_handler, constructor } =
133        extract_semantic_entrypoints(db, contract)?;
134    let SierraProgramWithDebug { program: mut sierra_program, debug_info } = db
135        .get_sierra_program_for_functions(
136            chain!(&external, &l1_handler, &constructor).map(|f| f.value).collect(),
137        )
138        .to_option()
139        .context("Compilation failed without any diagnostics.")?
140        .clone();
141
142    if compiler_config.replace_ids {
143        sierra_program = replace_sierra_ids_in_program(db, &sierra_program);
144    }
145    let replacer = CanonicalReplacer::from_program(&sierra_program);
146    let sierra_program = replacer.apply(&sierra_program);
147
148    let entry_points_by_type = ContractEntryPoints {
149        external: get_entry_points(db, &external, &replacer)?,
150        l1_handler: get_entry_points(db, &l1_handler, &replacer)?,
151        // Later generation of ABI verifies that there is up to one constructor.
152        constructor: get_entry_points(db, &constructor, &replacer)?,
153    };
154
155    let mut annotations = Annotations::default();
156
157    if compiler_config.add_statements_functions {
158        let statements_functions = debug_info.statements_locations.extract_statements_functions(db);
159        annotations.extend(Annotations::from(statements_functions))
160    };
161
162    if compiler_config.add_statements_code_locations {
163        let statements_functions =
164            debug_info.statements_locations.extract_statements_source_code_locations(db);
165        annotations.extend(Annotations::from(statements_functions))
166    };
167
168    let abi_builder: Option<AbiBuilder<'db>> =
169        AbiBuilder::from_submodule(db, contract.submodule_id, Default::default()).ok();
170    let finalized_abi =
171        abi_builder.with_context(|| "Unexpected error while generating ABI.")?.finalize();
172    let abi = match finalized_abi {
173        Ok(abi) => abi,
174        Err(e) => anyhow::bail!("Could not create ABI from contract submodule: {}", e),
175    };
176    let contract_class =
177        ContractClass::new(&sierra_program, entry_points_by_type, Some(abi), annotations)?;
178    contract_class.sanity_check();
179    Ok(contract_class)
180}
181
182pub struct SemanticEntryPoints<'db> {
183    pub external: Vec<Aliased<ConcreteFunctionWithBodyId<'db>>>,
184    pub l1_handler: Vec<Aliased<ConcreteFunctionWithBodyId<'db>>>,
185    pub constructor: Vec<Aliased<ConcreteFunctionWithBodyId<'db>>>,
186}
187
188/// Extracts functions from the contract.
189pub fn extract_semantic_entrypoints<'db>(
190    db: &'db dyn Database,
191    contract: &ContractDeclaration<'db>,
192) -> core::result::Result<SemanticEntryPoints<'db>, anyhow::Error> {
193    let external: Vec<_> = get_contract_abi_functions(db, contract, EXTERNAL_MODULE)?
194        .into_iter()
195        .map(|f| f.map(|f| ConcreteFunctionWithBodyId::from_semantic(db, f)))
196        .collect();
197    let l1_handler: Vec<_> = get_contract_abi_functions(db, contract, L1_HANDLER_MODULE)?
198        .into_iter()
199        .map(|f| f.map(|f| ConcreteFunctionWithBodyId::from_semantic(db, f)))
200        .collect();
201    let constructor: Vec<_> = get_contract_abi_functions(db, contract, CONSTRUCTOR_MODULE)?
202        .into_iter()
203        .map(|f| f.map(|f| ConcreteFunctionWithBodyId::from_semantic(db, f)))
204        .collect();
205    if constructor.len() > 1 {
206        anyhow::bail!("Expected at most one constructor.");
207    }
208    Ok(SemanticEntryPoints { external, l1_handler, constructor })
209}
210
211/// Returns the entry points given their IDs sorted by selectors.
212fn get_entry_points<'db>(
213    db: &'db dyn Database,
214    entry_point_functions: &[Aliased<ConcreteFunctionWithBodyId<'db>>],
215    replacer: &CanonicalReplacer,
216) -> Result<Vec<ContractEntryPoint>> {
217    let mut entry_points = vec![];
218    for function_with_body_id in entry_point_functions {
219        let (selector, sierra_id) =
220            get_selector_and_sierra_function(db, function_with_body_id, replacer);
221
222        entry_points.push(ContractEntryPoint {
223            selector: selector.to_biguint(),
224            function_idx: sierra_id.id as usize,
225        });
226    }
227    entry_points.sort_by(|a, b| a.selector.cmp(&b.selector));
228    Ok(entry_points)
229}
230
231/// Compile Starknet crate (or specific contract in the crate).
232pub fn starknet_compile(
233    crate_path: PathBuf,
234    contract_path: Option<String>,
235    config: Option<CompilerConfig<'_>>,
236    inlining_strategy: InliningStrategy,
237    allowed_libfuncs_list: Option<ListSelector>,
238) -> Result<String> {
239    let config = config.unwrap_or_default();
240    let contract = compile_path(&crate_path, contract_path.as_deref(), config, inlining_strategy)?;
241    contract.validate_version_compatible(allowed_libfuncs_list.unwrap_or_default())?;
242    serde_json::to_string_pretty(&contract).with_context(|| "Serialization failed.")
243}