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