cairo_lang_executable/
compile.rs

1use std::path::Path;
2
3use anyhow::{Context, Result};
4use cairo_lang_compiler::db::RootDatabase;
5use cairo_lang_compiler::diagnostics::DiagnosticsReporter;
6use cairo_lang_compiler::project::setup_project;
7use cairo_lang_compiler::{ensure_diagnostics, get_sierra_program_for_functions};
8use cairo_lang_debug::debug::DebugWithDb;
9use cairo_lang_executable_plugin::{
10    EXECUTABLE_PREFIX, EXECUTABLE_RAW_ATTR, executable_plugin_suite,
11};
12use cairo_lang_filesystem::cfg::{Cfg, CfgSet};
13use cairo_lang_filesystem::ids::{CrateId, CrateInput};
14use cairo_lang_lowering::ids::ConcreteFunctionWithBodyId;
15use cairo_lang_runnable_utils::builder::{
16    CasmProgramWrapperInfo, EntryCodeConfig, RunnableBuilder,
17};
18use cairo_lang_sierra_generator::db::SierraGenGroup;
19use cairo_lang_sierra_generator::debug_info::SierraProgramDebugInfo;
20use cairo_lang_sierra_generator::executables::find_executable_function_ids;
21use cairo_lang_sierra_generator::program_generator::SierraProgramWithDebug;
22use cairo_lang_sierra_to_casm::compiler::CairoProgram;
23use cairo_lang_utils::write_comma_separated;
24use itertools::Itertools;
25use salsa::Database;
26
27/// The CASM compilation result.
28pub struct CompiledFunction {
29    /// The compiled CASM program.
30    pub program: CairoProgram,
31    /// The wrapper information for the program.
32    pub wrapper: CasmProgramWrapperInfo,
33}
34impl std::fmt::Display for CompiledFunction {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        write!(f, "// builtins:")?;
37        if !self.wrapper.builtins.is_empty() {
38            write!(f, " ")?;
39            write_comma_separated(f, self.wrapper.builtins.iter().map(|b| b.to_str()))?;
40        }
41        writeln!(f)?;
42        writeln!(f, "// header")?;
43        for instruction in &self.wrapper.header {
44            writeln!(f, "{instruction};")?;
45        }
46        writeln!(f, "// sierra based code")?;
47        write!(f, "{}", self.program)?;
48        writeln!(f, "// footer")?;
49        for instruction in &self.wrapper.footer {
50            writeln!(f, "{instruction};")?;
51        }
52        Ok(())
53    }
54}
55
56#[derive(Debug, Clone, Default)]
57pub struct ExecutableConfig {
58    /// If true, will allow syscalls in the program.
59    ///
60    /// In general, syscalls are not allowed in executables, as they are currently not verified.
61    pub allow_syscalls: bool,
62
63    /// Replace the panic flow with an unprovable opcode, this reduces code size but might make it
64    /// more difficult to debug.
65    pub unsafe_panic: bool,
66}
67
68/// Represents the output of compiling an executable.
69///
70/// Includes the `CompiledFunction` along with supplementary objects useful for profiling.
71pub struct CompileExecutableResult<'db> {
72    /// The compiled function.
73    pub compiled_function: CompiledFunction,
74    /// A runnable builder with the program corresponding to the compiled function.
75    pub builder: RunnableBuilder,
76    /// The debug info for the Sierra program in the builder.
77    pub debug_info: SierraProgramDebugInfo<'db>,
78}
79
80/// Prepares a `RootDatabase` configured for compiling `#[executable]` functions.
81///
82/// This only builds the database; use `setup_project` and the compilation helpers
83/// on top of it to actually compile code.
84pub fn prepare_db(config: &ExecutableConfig) -> Result<RootDatabase> {
85    let mut builder = RootDatabase::builder();
86    builder
87        .skip_auto_withdraw_gas()
88        .with_cfg(CfgSet::from_iter([Cfg::kv("gas", "disabled")]))
89        .detect_corelib()
90        .with_default_plugin_suite(executable_plugin_suite());
91    if config.unsafe_panic {
92        builder.with_unsafe_panic();
93    }
94
95    builder.build()
96}
97
98/// Compile the function given by path.
99/// Errors if there is ambiguity.
100pub fn compile_executable<'db>(
101    db: &'db mut dyn Database,
102    path: &Path,
103    executable_path: Option<&str>,
104    diagnostics_reporter: DiagnosticsReporter<'_>,
105    config: ExecutableConfig,
106) -> Result<CompileExecutableResult<'db>> {
107    // let mut db = prepare_db(&config)?;
108
109    let main_crate_inputs = setup_project(db, Path::new(&path))?;
110    let diagnostics_reporter = diagnostics_reporter.with_crates(&main_crate_inputs);
111    let main_crate_ids = CrateInput::into_crate_ids(db, main_crate_inputs);
112
113    compile_executable_in_prepared_db(
114        db,
115        executable_path,
116        main_crate_ids,
117        diagnostics_reporter,
118        config,
119    )
120}
121
122/// Runs compiler on the specified executable function.
123/// If no executable was specified, verify that there is only one.
124/// Otherwise, return an error.
125pub fn compile_executable_in_prepared_db<'db>(
126    db: &'db dyn Database,
127    executable_path: Option<&str>,
128    main_crate_ids: Vec<CrateId<'db>>,
129    mut diagnostics_reporter: DiagnosticsReporter<'_>,
130    config: ExecutableConfig,
131) -> Result<CompileExecutableResult<'db>> {
132    ensure_diagnostics(db, &mut diagnostics_reporter)?;
133
134    let executables = find_executable_functions(db, main_crate_ids, executable_path);
135
136    match executables.iter().exactly_one() {
137        Ok(executable) => compile_executable_function_in_prepared_db(db, *executable, config),
138        Err(_) if executables.is_empty() => {
139            anyhow::bail!("Requested `#[executable]` not found.");
140        }
141        Err(_) => {
142            let executable_names = executables
143                .into_iter()
144                .map(|executable| originating_function_path(db, executable))
145                .join("\n  ");
146            anyhow::bail!(
147                "More than one executable found in the main crate: \n  {}\nUse --executable to \
148                 specify which to compile.",
149                executable_names
150            );
151        }
152    }
153}
154
155/// Search crates identified by `main_crate_ids` for executable functions.
156/// If `executable_path` is provided, only functions with exactly the same path will be returned.
157pub fn find_executable_functions<'db>(
158    db: &'db dyn Database,
159    main_crate_ids: Vec<CrateId<'db>>,
160    executable_path: Option<&str>,
161) -> Vec<ConcreteFunctionWithBodyId<'db>> {
162    let mut executables: Vec<_> = find_executable_function_ids(db, main_crate_ids)
163        .into_iter()
164        .filter_map(|(id, labels)| {
165            labels.into_iter().any(|ssid| ssid.long(db) == EXECUTABLE_RAW_ATTR).then_some(id)
166        })
167        .collect();
168
169    if let Some(executable_path) = executable_path {
170        executables
171            .retain(|executable| originating_function_path(db, *executable) == executable_path);
172    };
173    executables
174}
175
176/// Returns the path to the function that the executable is wrapping.
177///
178/// If the executable is not wrapping a function, returns the full path of the executable.
179pub fn originating_function_path<'db>(
180    db: &'db dyn Database,
181    wrapper: ConcreteFunctionWithBodyId<'db>,
182) -> String {
183    let semantic = wrapper.base_semantic_function(db);
184    let wrapper_name = semantic.name(db).long(db).as_str();
185    let wrapper_full_path = semantic.full_path(db);
186    let Some(wrapped_name) = wrapper_name.strip_prefix(EXECUTABLE_PREFIX) else {
187        return wrapper_full_path;
188    };
189    let Some(wrapper_path_to_module) = wrapper_full_path.strip_suffix(wrapper_name) else {
190        return wrapper_full_path;
191    };
192    format!("{wrapper_path_to_module}{wrapped_name}")
193}
194
195/// Runs the executable compiler on the specified function in a prepared database.
196///
197/// Diagnostics are expected to have been checked by the caller (for example via
198/// [`compile_executable_in_prepared_db`]).
199///
200/// # Arguments
201/// * `db` - Preloaded compilation database.
202/// * `executable` - The [`ConcreteFunctionWithBodyId`] to compile.
203/// * `config` - Configuration for executables (e.g. syscall allowance and panic behavior).
204/// # Returns
205/// * `Ok(CompileExecutableResult<'db>)` - The compiled CASM program and associated metadata.
206/// * `Err(anyhow::Error)` - Compilation failed.
207pub fn compile_executable_function_in_prepared_db<'db>(
208    db: &'db dyn Database,
209    executable: ConcreteFunctionWithBodyId<'db>,
210    config: ExecutableConfig,
211) -> Result<CompileExecutableResult<'db>> {
212    let SierraProgramWithDebug { program: sierra_program, debug_info } =
213        get_sierra_program_for_functions(db, vec![executable])
214            .with_context(|| "Compilation failed without any diagnostics.")?;
215    if !config.allow_syscalls {
216        // Finding if any syscall libfuncs are used in the program.
217        // If any are found, the compilation will fail, as syscalls are not proved in executables.
218        for libfunc in &sierra_program.libfunc_declarations {
219            if libfunc.long_id.generic_id.0.ends_with("_syscall") {
220                anyhow::bail!(
221                    "The function is using libfunc `{}`. Syscalls are not supported in \
222                     `#[executable]`.",
223                    libfunc.long_id.generic_id
224                );
225            }
226        }
227    }
228
229    // Since we build the entry point asking for a single function - we know it will be first, and
230    // that it will be available.
231    let executable_func = sierra_program.funcs[0].clone();
232    assert_eq!(executable_func.id, db.intern_sierra_function(executable.function_id(db).unwrap()));
233    let builder = RunnableBuilder::new(sierra_program.clone(), None).map_err(|err| {
234        let mut locs = vec![];
235        for stmt_idx in err.stmt_indices() {
236            if let Some(loc) =
237                debug_info.statements_locations.statement_diagnostic_location(db, stmt_idx)
238            {
239                locs.push(format!("#{stmt_idx} {:?}", loc.debug(db)))
240            }
241        }
242        anyhow::anyhow!("Failed to create runnable builder: {}\n{}", err, locs.join("\n"))
243    })?;
244
245    // If syscalls are allowed it means we allow for unsound programs.
246    let allow_unsound = config.allow_syscalls;
247    let wrapper = builder
248        .create_wrapper_info(&executable_func, EntryCodeConfig::executable(allow_unsound))?;
249    let compiled_function = CompiledFunction { program: builder.casm_program().clone(), wrapper };
250    Ok(CompileExecutableResult { compiled_function, builder, debug_info: debug_info.clone() })
251}