cairo_lang_starknet/
compile.rs

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