cairo_lang_runnable_utils/
builder.rs

1use cairo_lang_casm::assembler::AssembledCairoProgram;
2use cairo_lang_casm::builder::CasmBuilder;
3use cairo_lang_casm::cell_expression::CellExpression;
4use cairo_lang_casm::hints::ExternalHint;
5use cairo_lang_casm::instructions::Instruction;
6use cairo_lang_casm::{casm, casm_build_extend, deref};
7use cairo_lang_sierra::extensions::bitwise::BitwiseType;
8use cairo_lang_sierra::extensions::circuit::{AddModType, MulModType};
9use cairo_lang_sierra::extensions::core::{CoreLibfunc, CoreType};
10use cairo_lang_sierra::extensions::ec::EcOpType;
11use cairo_lang_sierra::extensions::gas::GasBuiltinType;
12use cairo_lang_sierra::extensions::pedersen::PedersenType;
13use cairo_lang_sierra::extensions::poseidon::PoseidonType;
14use cairo_lang_sierra::extensions::range_check::{RangeCheck96Type, RangeCheckType};
15use cairo_lang_sierra::extensions::segment_arena::SegmentArenaType;
16use cairo_lang_sierra::extensions::starknet::syscalls::SystemType;
17use cairo_lang_sierra::extensions::{ConcreteType, NamedType};
18use cairo_lang_sierra::ids::{ConcreteTypeId, GenericTypeId};
19use cairo_lang_sierra::program::{
20    ConcreteTypeLongId, Function, Program as SierraProgram, StatementIdx,
21};
22use cairo_lang_sierra::program_registry::{ProgramRegistry, ProgramRegistryError};
23use cairo_lang_sierra_ap_change::ApChangeError;
24use cairo_lang_sierra_gas::CostError;
25use cairo_lang_sierra_to_casm::compiler::{CairoProgram, CompilationError, SierraToCasmConfig};
26use cairo_lang_sierra_to_casm::metadata::{
27    Metadata, MetadataComputationConfig, MetadataError, calc_metadata, calc_metadata_ap_change_only,
28};
29use cairo_lang_sierra_type_size::{TypeSizeMap, get_type_size_map};
30use cairo_lang_utils::casts::IntoOrPanic;
31use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
32use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
33use cairo_lang_utils::unordered_hash_set::UnorderedHashSet;
34use cairo_vm::types::builtin_name::BuiltinName;
35use itertools::{chain, zip_eq};
36use thiserror::Error;
37
38#[derive(Debug, Error)]
39pub enum BuildError {
40    #[error(
41        "Failed calculating gas usage, it is likely a call for `gas::withdraw_gas` is missing. \
42         Inner error: {0}"
43    )]
44    FailedGasCalculation(#[from] CostError),
45    #[error("Function with suffix `{suffix}` to run not found.")]
46    MissingFunction { suffix: String },
47    #[error(transparent)]
48    ProgramRegistryError(#[from] Box<ProgramRegistryError>),
49    #[error(transparent)]
50    SierraCompilationError(#[from] Box<CompilationError>),
51    #[error(transparent)]
52    ApChangeError(#[from] ApChangeError),
53}
54
55impl BuildError {
56    pub fn stmt_indices(&self) -> Vec<StatementIdx> {
57        match self {
58            BuildError::SierraCompilationError(err) => err.stmt_indices(),
59            _ => vec![],
60        }
61    }
62}
63
64/// Builder for creating a runnable CASM program from Sierra code.
65pub struct RunnableBuilder {
66    /// The sierra program.
67    sierra_program: SierraProgram,
68    /// Metadata for the Sierra program.
69    metadata: Metadata,
70    /// Program registry for the Sierra program.
71    sierra_program_registry: ProgramRegistry<CoreType, CoreLibfunc>,
72    /// Program registry for the Sierra program.
73    type_sizes: TypeSizeMap,
74    /// The casm program matching the Sierra code.
75    casm_program: CairoProgram,
76    /// The types of the non-user argument variables.
77    non_args_types: UnorderedHashSet<GenericTypeId>,
78}
79
80impl RunnableBuilder {
81    /// Creates a new `RunnableBuilder` for a Sierra program.
82    pub fn new(
83        sierra_program: SierraProgram,
84        metadata_config: Option<MetadataComputationConfig>,
85    ) -> Result<Self, BuildError> {
86        let gas_usage_check = metadata_config.is_some();
87        let metadata = create_metadata(&sierra_program, metadata_config)?;
88        let sierra_program_registry =
89            ProgramRegistry::<CoreType, CoreLibfunc>::new(&sierra_program)?;
90        let type_sizes = get_type_size_map(&sierra_program, &sierra_program_registry).unwrap();
91        let casm_program = cairo_lang_sierra_to_casm::compiler::compile(
92            &sierra_program,
93            &metadata,
94            SierraToCasmConfig { gas_usage_check, max_bytecode_size: usize::MAX },
95        )?;
96
97        Ok(Self {
98            sierra_program,
99            metadata,
100            sierra_program_registry,
101            type_sizes,
102            casm_program,
103            non_args_types: UnorderedHashSet::from_iter([
104                AddModType::ID,
105                BitwiseType::ID,
106                GasBuiltinType::ID,
107                EcOpType::ID,
108                MulModType::ID,
109                PedersenType::ID,
110                PoseidonType::ID,
111                RangeCheck96Type::ID,
112                RangeCheckType::ID,
113                SegmentArenaType::ID,
114                SystemType::ID,
115            ]),
116        })
117    }
118
119    /// Returns the Sierra program.
120    pub fn sierra_program(&self) -> &SierraProgram {
121        &self.sierra_program
122    }
123
124    /// Returns the CASM program of the Sierra program, without any modifications.
125    pub fn casm_program(&self) -> &CairoProgram {
126        &self.casm_program
127    }
128
129    /// Returns the metadata of the Sierra program.
130    pub fn metadata(&self) -> &Metadata {
131        &self.metadata
132    }
133
134    /// Returns the program registry of the Sierra program.
135    pub fn registry(&self) -> &ProgramRegistry<CoreType, CoreLibfunc> {
136        &self.sierra_program_registry
137    }
138
139    /// Finds first function ending with `name_suffix`.
140    pub fn find_function(&self, name_suffix: &str) -> Result<&Function, BuildError> {
141        self.sierra_program
142            .funcs
143            .iter()
144            .find(|f| {
145                if let Some(name) = &f.id.debug_name { name.ends_with(name_suffix) } else { false }
146            })
147            .ok_or_else(|| BuildError::MissingFunction { suffix: name_suffix.to_owned() })
148    }
149
150    /// Returns the type info of a given `ConcreteTypeId`.
151    fn type_info(&self, ty: &ConcreteTypeId) -> &cairo_lang_sierra::extensions::types::TypeInfo {
152        self.sierra_program_registry.get_type(ty).unwrap().info()
153    }
154
155    /// Returns the long id of a given `ConcreteTypeId`.
156    pub fn type_long_id(&self, ty: &ConcreteTypeId) -> &ConcreteTypeLongId {
157        &self.type_info(ty).long_id
158    }
159
160    /// Returns the long id of a given `ConcreteTypeId`.
161    pub fn type_size(&self, ty: &ConcreteTypeId) -> i16 {
162        self.type_sizes[ty]
163    }
164
165    /// Returns whether `ty` is a user arg sort of type.
166    pub fn is_user_arg_type(&self, ty: &GenericTypeId) -> bool {
167        !self.non_args_types.contains(ty)
168    }
169
170    /// Creates the wrapper info for a given function.
171    pub fn create_wrapper_info(
172        &self,
173        func: &Function,
174        config: EntryCodeConfig,
175    ) -> Result<CasmProgramWrapperInfo, BuildError> {
176        let (header, builtins) = self.create_entry_code(func, config)?;
177        Ok(CasmProgramWrapperInfo { header, builtins, footer: create_code_footer() })
178    }
179
180    /// Assembles a function program for a given function.
181    pub fn assemble_function_program(
182        &self,
183        func: &Function,
184        config: EntryCodeConfig,
185    ) -> Result<(AssembledCairoProgram, Vec<BuiltinName>), BuildError> {
186        let info = self.create_wrapper_info(func, config)?;
187        let assembled_cairo_program = self.casm_program.assemble_ex(&info.header, &info.footer);
188        Ok((assembled_cairo_program, info.builtins))
189    }
190
191    /// Returns the instructions to add to the beginning of the code to successfully call the main
192    /// function, as well as the builtins required to execute the program.
193    fn create_entry_code(
194        &self,
195        func: &Function,
196        config: EntryCodeConfig,
197    ) -> Result<(Vec<Instruction>, Vec<BuiltinName>), BuildError> {
198        let param_types = self.generic_id_and_size_from_concrete(&func.signature.param_types);
199        let return_types = self.generic_id_and_size_from_concrete(&func.signature.ret_types);
200
201        let entry_point = func.entry_point.0;
202        let code_offset =
203            self.casm_program.debug_info.sierra_statement_info[entry_point].start_offset;
204        // Finalizing for proof only if all returned values are builtins or droppable.
205        let droppable_return_value = func.signature.ret_types.iter().all(|ty| {
206            let info = self.type_info(ty);
207            info.droppable || !self.is_user_arg_type(&info.long_id.generic_id)
208        });
209        if !droppable_return_value {
210            assert!(
211                !config.finalize_segment_arena,
212                "Cannot finalize the segment arena when returning non-droppable values."
213            );
214        }
215
216        create_entry_code_from_params(&param_types, &return_types, code_offset, config)
217    }
218
219    /// Converts array of `ConcreteTypeId`s into corresponding `GenericTypeId`s and their sizes
220    pub fn generic_id_and_size_from_concrete(
221        &self,
222        types: &[ConcreteTypeId],
223    ) -> Vec<(GenericTypeId, i16)> {
224        types
225            .iter()
226            .map(|pt| {
227                let info = self.type_info(pt);
228                let generic_id = &info.long_id.generic_id;
229                let size = self.type_sizes[pt];
230                (generic_id.clone(), size)
231            })
232            .collect()
233    }
234}
235
236/// Configuration for the entry code creation.
237#[derive(Clone, Debug)]
238pub struct EntryCodeConfig {
239    /// Whether to finalize the segment arena after calling the function.
240    pub finalize_segment_arena: bool,
241    /// Whether the wrapped function is expecting the output builtin.
242    ///
243    /// If true, will expect the function signature to be `(Span<felt252>, Array<felt252>) ->
244    /// Array<felt252>`. And will inject the output builtin to be the supplied array input, and
245    /// to be the result of the output.
246    pub outputting_function: bool,
247}
248impl EntryCodeConfig {
249    /// Returns a configuration for testing purposes.
250    ///
251    /// This configuration will not finalize the segment arena after calling the function, to
252    /// prevent failure in case of functions returning values.
253    pub fn testing() -> Self {
254        Self { finalize_segment_arena: false, outputting_function: false }
255    }
256
257    /// Returns a configuration for execution purposes.
258    pub fn executable() -> Self {
259        Self { finalize_segment_arena: true, outputting_function: true }
260    }
261}
262
263/// Information about a CASM program.
264pub struct CasmProgramWrapperInfo {
265    /// The builtins used in the program.
266    pub builtins: Vec<BuiltinName>,
267    /// The instructions before the program.
268    pub header: Vec<Instruction>,
269    /// The instructions after the program.
270    pub footer: Vec<Instruction>,
271}
272
273/// Returns the entry code to call the function with `param_types` as its inputs and
274/// `return_types` as outputs, located at `code_offset`. If `finalize_for_proof` is true,
275/// will make sure to remove the segment arena after calling the function. For testing purposes,
276/// `finalize_for_proof` can be set to false, to avoid a failure of the segment arena validation.
277pub fn create_entry_code_from_params(
278    param_types: &[(GenericTypeId, i16)],
279    return_types: &[(GenericTypeId, i16)],
280    code_offset: usize,
281    config: EntryCodeConfig,
282) -> Result<(Vec<Instruction>, Vec<BuiltinName>), BuildError> {
283    let mut ctx = CasmBuilder::default();
284    let mut builtin_offset = 3;
285    let mut builtin_vars = OrderedHashMap::<_, _>::default();
286    let mut builtin_ty_to_vm_name = UnorderedHashMap::<_, _>::default();
287    let mut builtins = vec![];
288    for (builtin_name, builtin_ty) in [
289        (BuiltinName::mul_mod, MulModType::ID),
290        (BuiltinName::add_mod, AddModType::ID),
291        (BuiltinName::range_check96, RangeCheck96Type::ID),
292        (BuiltinName::poseidon, PoseidonType::ID),
293        (BuiltinName::ec_op, EcOpType::ID),
294        (BuiltinName::bitwise, BitwiseType::ID),
295        (BuiltinName::range_check, RangeCheckType::ID),
296        (BuiltinName::pedersen, PedersenType::ID),
297    ] {
298        if param_types.iter().any(|(ty, _)| ty == &builtin_ty) {
299            // The offset [fp - i] for each of this builtins in this configuration.
300            builtin_vars.insert(
301                builtin_name,
302                ctx.add_var(CellExpression::Deref(deref!([fp - builtin_offset]))),
303            );
304            builtin_ty_to_vm_name.insert(builtin_ty.clone(), builtin_name);
305            builtin_offset += 1;
306            builtins.push(builtin_name);
307        }
308    }
309    if config.outputting_function {
310        let output_builtin_var = ctx.add_var(CellExpression::Deref(deref!([fp - builtin_offset])));
311        builtin_vars.insert(BuiltinName::output, output_builtin_var);
312        builtins.push(BuiltinName::output);
313    }
314    builtins.reverse();
315
316    let emulated_builtins = UnorderedHashSet::<_>::from_iter([SystemType::ID]);
317
318    let got_segment_arena = param_types.iter().any(|(ty, _)| ty == &SegmentArenaType::ID);
319    let has_post_calculation_loop = got_segment_arena && config.finalize_segment_arena;
320
321    let mut local_exprs = vec![];
322    if has_post_calculation_loop {
323        let mut allocate_cell = || {
324            casm_build_extend!(ctx, localvar local;);
325            local_exprs.push(ctx.get_value(local, false));
326        };
327        // In `outputting_function` case, the return data does not need to be returned to the
328        // caller.
329        if !config.outputting_function {
330            for (ty, size) in return_types {
331                // Builtins are handled later.
332                if !builtin_ty_to_vm_name.contains_key(ty) {
333                    for _ in 0..*size {
334                        allocate_cell();
335                    }
336                }
337            }
338        }
339        for name in builtin_vars.keys() {
340            if name != &BuiltinName::segment_arena {
341                allocate_cell();
342            }
343        }
344        if !local_exprs.is_empty() {
345            casm_build_extend!(ctx, ap += local_exprs.len(););
346        }
347    }
348    if got_segment_arena {
349        casm_build_extend! {ctx,
350            tempvar segment_arena;
351            tempvar infos;
352            hint AllocSegment into {dst: segment_arena};
353            hint AllocSegment into {dst: infos};
354            const czero = 0;
355            tempvar zero = czero;
356            // Write Infos segment, n_constructed (0), and n_destructed (0) to the segment.
357            assert infos = *(segment_arena++);
358            assert zero = *(segment_arena++);
359            assert zero = *(segment_arena++);
360        }
361        // Adding the segment arena to the builtins var map.
362        builtin_vars.insert(BuiltinName::segment_arena, segment_arena);
363        builtin_ty_to_vm_name.insert(SegmentArenaType::ID, BuiltinName::segment_arena);
364    }
365    let mut unallocated_count = 0;
366    let mut param_index = 0;
367    for (generic_ty, ty_size) in param_types {
368        if let Some(name) = builtin_ty_to_vm_name.get(generic_ty).cloned() {
369            let var = builtin_vars[&name];
370            casm_build_extend!(ctx, tempvar _builtin = var;);
371        } else if emulated_builtins.contains(generic_ty) {
372            casm_build_extend! {ctx,
373                tempvar system;
374                hint AllocSegment into {dst: system};
375            };
376            unallocated_count += ty_size;
377        } else if !config.outputting_function {
378            if *ty_size > 0 {
379                casm_build_extend! {ctx,
380                    tempvar first;
381                    const param_index = param_index;
382                    hint ExternalHint::WriteRunParam { index: param_index } into { dst: first };
383                };
384                for _ in 1..*ty_size {
385                    casm_build_extend!(ctx, tempvar _cell;);
386                }
387            }
388            param_index += 1;
389            unallocated_count += ty_size;
390        } else if generic_ty == &GasBuiltinType::ID {
391            // Setting gas to be far from u64 boundaries.
392            casm_build_extend! {ctx,
393                const max_gas = i64::MAX;
394                tempvar gas = max_gas;
395            };
396        }
397    }
398    if config.outputting_function {
399        let output_ptr = builtin_vars[&BuiltinName::output];
400        casm_build_extend! { ctx,
401            tempvar input_start;
402            tempvar _input_end;
403            const param_index = 0;
404            hint ExternalHint::WriteRunParam { index: param_index } into { dst: input_start };
405            const user_data_offset = 1;
406            tempvar output_start = output_ptr + user_data_offset;
407            tempvar output_end = output_start;
408        };
409        unallocated_count += 2;
410    }
411    if unallocated_count > 0 {
412        casm_build_extend!(ctx, ap += unallocated_count.into_or_panic::<usize>(););
413    }
414    casm_build_extend! (ctx, let () = call FUNCTION;);
415    let mut unprocessed_return_size = return_types.iter().map(|(_, size)| size).sum::<i16>();
416    let mut next_unprocessed_deref = || {
417        let deref_cell = CellExpression::Deref(deref!([ap - unprocessed_return_size]));
418        unprocessed_return_size -= 1;
419        deref_cell
420    };
421    let mut return_data = vec![];
422    for (ret_ty, size) in return_types {
423        if let Some(name) = builtin_ty_to_vm_name.get(ret_ty) {
424            *builtin_vars.get_mut(name).unwrap() = ctx.add_var(next_unprocessed_deref());
425        } else if config.outputting_function {
426            if ret_ty == &GasBuiltinType::ID {
427                next_unprocessed_deref();
428                continue;
429            }
430            let output_ptr_var = builtin_vars[&BuiltinName::output];
431            // The output builtin values.
432            let new_output_ptr = if *size == 3 {
433                let panic_indicator = ctx.add_var(next_unprocessed_deref());
434                // The start ptr of the output in the case of successful run,
435                // or the panic data in case of a failure.
436                let ptr_start = ctx.add_var(next_unprocessed_deref());
437                // The end ptr of the output in the case of successful run,
438                // or the panic data in case of a failure.
439                let ptr_end = ctx.add_var(next_unprocessed_deref());
440                casm_build_extend! {ctx,
441                    tempvar new_output_ptr;
442                    jump PANIC if panic_indicator != 0;
443                    // SUCCESS:
444                    assert new_output_ptr = ptr_end;
445                    jump AFTER_PANIC_HANDLING;
446                    PANIC:
447                    // Marking the panic message in case of a panic.
448                    hint ExternalHint::SetMarker { marker: ptr_start };
449                    hint ExternalHint::SetMarker { marker: ptr_end };
450                    const one = 1;
451                    // In the case of an error, we assume no values are written to the output_ptr.
452                    assert new_output_ptr = output_ptr_var + one;
453                    AFTER_PANIC_HANDLING:
454                    assert panic_indicator = output_ptr_var[0];
455                };
456                new_output_ptr
457            } else if *size == 2 {
458                // No panic possible.
459                next_unprocessed_deref();
460                let output_ptr_end = ctx.add_var(next_unprocessed_deref());
461                casm_build_extend! {ctx,
462                    const czero = 0;
463                    tempvar zero = czero;
464                    assert zero = *(output_ptr_var++);
465                };
466                output_ptr_end
467            } else {
468                panic!("Unexpected output size: {size}",);
469            };
470            *builtin_vars.get_mut(&BuiltinName::output).unwrap() = new_output_ptr;
471        } else {
472            for _ in 0..*size {
473                return_data.push(ctx.add_var(next_unprocessed_deref()));
474            }
475        }
476    }
477    assert_eq!(unprocessed_return_size, 0);
478    if has_post_calculation_loop {
479        // Storing local data on FP - as we have a loop now.
480        // Note that `return_data` is empty in `outputting_function` mode.
481        let saved_post_loop =
482            chain!(&return_data, builtin_vars.iter().filter_map(non_segment_arena_var));
483        for (cell, local_expr) in zip_eq(saved_post_loop.cloned(), &local_exprs) {
484            let local_cell = ctx.add_var(local_expr.clone());
485            casm_build_extend!(ctx, assert local_cell = cell;);
486        }
487    }
488    if got_segment_arena && config.finalize_segment_arena {
489        let segment_arena = builtin_vars[&BuiltinName::segment_arena];
490        // Validating the segment arena's segments are one after the other.
491        casm_build_extend! {ctx,
492            tempvar n_segments = segment_arena[-2];
493            tempvar n_finalized = segment_arena[-1];
494            assert n_segments = n_finalized;
495            jump STILL_LEFT_PRE if n_segments != 0;
496            rescope{};
497            jump DONE_VALIDATION;
498            STILL_LEFT_PRE:
499            const one = 1;
500            tempvar infos = segment_arena[-3];
501            tempvar remaining_segments = n_segments - one;
502            rescope{infos = infos, remaining_segments = remaining_segments};
503            LOOP_START:
504            jump STILL_LEFT_LOOP if remaining_segments != 0;
505            rescope{};
506            jump DONE_VALIDATION;
507            STILL_LEFT_LOOP:
508            tempvar prev_end = infos[1];
509            tempvar curr_start = infos[3];
510            const one = 1;
511            let expected_curr_start = prev_end + one;
512            hint ExternalHint::AddRelocationRule { src: curr_start, dst: expected_curr_start };
513            assert curr_start = prev_end + one;
514            const three = 3;
515            tempvar next_infos = infos + three;
516            tempvar next_remaining_segments = remaining_segments - one;
517            rescope{infos = next_infos, remaining_segments = next_remaining_segments};
518            #{ steps = 0; }
519            jump LOOP_START;
520            DONE_VALIDATION:
521        };
522    }
523    if has_post_calculation_loop {
524        let mut locals = local_exprs.into_iter();
525        // Copying the result back to AP, for consistency of the result location.
526        // Note that `return_data` is empty in `outputting_function` mode.
527        for _ in 0..return_data.len() {
528            let local_cell = ctx.add_var(locals.next().unwrap().clone());
529            casm_build_extend!(ctx, tempvar _cell = local_cell;);
530        }
531        for (var, local_expr) in
532            zip_eq(builtin_vars.iter_mut().filter_map(non_segment_arena_var), locals)
533        {
534            *var = ctx.add_var(local_expr);
535        }
536    }
537    if config.outputting_function {
538        for name in [
539            BuiltinName::output,
540            BuiltinName::pedersen,
541            BuiltinName::range_check,
542            BuiltinName::bitwise,
543            BuiltinName::ec_op,
544            BuiltinName::poseidon,
545            BuiltinName::range_check96,
546            BuiltinName::add_mod,
547            BuiltinName::mul_mod,
548        ] {
549            if let Some(var) = builtin_vars.get(&name).copied() {
550                casm_build_extend!(ctx, tempvar cell = var;);
551            }
552        }
553    }
554    casm_build_extend! (ctx, ret;);
555    ctx.future_label("FUNCTION".into(), code_offset);
556    Ok((ctx.build([]).instructions, builtins))
557}
558
559/// Helper to filter out the segment arena from a builtin vars map.
560fn non_segment_arena_var<T>((name, var): (&BuiltinName, T)) -> Option<T> {
561    if *name == BuiltinName::segment_arena { None } else { Some(var) }
562}
563
564/// Creates a list of instructions that will be appended to the program's bytecode.
565pub fn create_code_footer() -> Vec<Instruction> {
566    casm! {
567        // Add a `ret` instruction used in libfuncs that retrieve the current value of the `fp`
568        // and `pc` registers.
569        ret;
570    }
571    .instructions
572}
573
574/// Creates the metadata required for a Sierra program lowering to casm.
575fn create_metadata(
576    sierra_program: &SierraProgram,
577    metadata_config: Option<MetadataComputationConfig>,
578) -> Result<Metadata, BuildError> {
579    if let Some(metadata_config) = metadata_config {
580        calc_metadata(sierra_program, metadata_config)
581    } else {
582        calc_metadata_ap_change_only(sierra_program)
583    }
584    .map_err(|err| match err {
585        MetadataError::ApChangeError(err) => BuildError::ApChangeError(err),
586        MetadataError::CostError(err) => BuildError::FailedGasCalculation(err),
587    })
588}