cairo_lang_executable/
compile.rs

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