cairo_lang_lowering/panic/
mod.rs

1use std::collections::VecDeque;
2
3use assert_matches::assert_matches;
4use cairo_lang_diagnostics::Maybe;
5use cairo_lang_filesystem::flag::{Flag, flag_unsafe_panic};
6use cairo_lang_filesystem::ids::FlagId;
7use cairo_lang_semantic::corelib::{
8    core_submodule, get_core_enum_concrete_variant, get_function_id, get_panic_ty, never_ty,
9};
10use cairo_lang_semantic::helper::ModuleHelper;
11use cairo_lang_semantic::items::constant::ConstValue;
12use cairo_lang_semantic::{self as semantic, GenericArgumentId};
13use cairo_lang_utils::{Intern, Upcast};
14use itertools::{Itertools, chain, zip_eq};
15use semantic::{ConcreteVariant, MatchArmSelector, TypeId};
16
17use crate::blocks::BlocksBuilder;
18use crate::db::{ConcreteSCCRepresentative, LoweringGroup};
19use crate::ids::{
20    ConcreteFunctionWithBodyId, FunctionId, FunctionLongId, LocationId, SemanticFunctionIdEx,
21    Signature,
22};
23use crate::lower::context::{VarRequest, VariableAllocator};
24use crate::{
25    Block, BlockEnd, BlockId, DependencyType, Lowered, LoweringStage, MatchArm, MatchEnumInfo,
26    MatchExternInfo, MatchInfo, Statement, StatementCall, StatementEnumConstruct,
27    StatementStructConstruct, StatementStructDestructure, VarRemapping, VarUsage, VariableId,
28};
29
30// TODO(spapini): Remove tuple in the Ok() variant of the panic, by supporting multiple values in
31// the Sierra type.
32
33/// Lowering phase that converts `BlockEnd::Panic` into `BlockEnd::Return`, and wraps necessary
34/// types with `PanicResult<>`.
35pub fn lower_panics(
36    db: &dyn LoweringGroup,
37    function_id: ConcreteFunctionWithBodyId,
38    lowered: &mut Lowered,
39) -> Maybe<()> {
40    // Skip this phase for non panicable functions.
41    if !db.function_with_body_may_panic(function_id)? {
42        return Ok(());
43    }
44
45    let opt_trace_fn = if matches!(
46        db.get_flag(FlagId::new(db, "panic_backtrace")),
47        Some(flag) if matches!(*flag, Flag::PanicBacktrace(true)),
48    ) {
49        Some(
50            ModuleHelper::core(db)
51                .submodule("internal")
52                .function_id(
53                    "trace",
54                    vec![GenericArgumentId::Constant(
55                        ConstValue::Int(
56                            0x70616e6963u64.into(), // 'panic' as numeric.
57                            db.core_info().felt252,
58                        )
59                        .intern(db),
60                    )],
61                )
62                .lowered(db),
63        )
64    } else {
65        None
66    };
67
68    if flag_unsafe_panic(db) {
69        lower_unsafe_panic(db, lowered, opt_trace_fn);
70        return Ok(());
71    }
72
73    let signature = function_id.signature(db)?;
74    // TODO(orizi): Validate all signature types are fully concrete at this point.
75    let panic_info = PanicSignatureInfo::new(db, &signature);
76    let variables = VariableAllocator::new(
77        db,
78        function_id.base_semantic_function(db).function_with_body_id(db),
79        std::mem::take(&mut lowered.variables),
80    )
81    .unwrap();
82    let mut ctx = PanicLoweringContext {
83        variables,
84        block_queue: VecDeque::from(lowered.blocks.get().clone()),
85        flat_blocks: BlocksBuilder::new(),
86        panic_info,
87    };
88
89    if let Some(trace_fn) = opt_trace_fn {
90        for block in ctx.block_queue.iter_mut() {
91            if let BlockEnd::Panic(end) = &block.end {
92                block.statements.push(Statement::Call(StatementCall {
93                    function: trace_fn,
94                    inputs: vec![],
95                    with_coupon: false,
96                    outputs: vec![],
97                    location: end.location,
98                }));
99            }
100        }
101    }
102
103    // Iterate block queue (old and new blocks).
104    while let Some(block) = ctx.block_queue.pop_front() {
105        ctx = handle_block(ctx, block)?;
106    }
107
108    lowered.variables = ctx.variables.variables;
109    lowered.blocks = ctx.flat_blocks.build().unwrap();
110
111    Ok(())
112}
113
114/// Lowering phase that converts BlockEnd::Panic into BlockEnd::Match { function: unsafe_panic }.
115/// 'opt_trace_fn' is an optional function to call before the panic.
116fn lower_unsafe_panic(
117    db: &dyn LoweringGroup,
118    lowered: &mut Lowered,
119    opt_trace_fn: Option<FunctionId>,
120) {
121    let panics = core_submodule(db, "panics");
122    let panic_func_id =
123        FunctionLongId::Semantic(get_function_id(db, panics, "unsafe_panic".into(), vec![]))
124            .intern(db);
125
126    for block in lowered.blocks.iter_mut() {
127        let BlockEnd::Panic(err_data) = &mut block.end else {
128            continue;
129        };
130
131        // Clean up undroppable panic related variables to pass add_destructs.
132        let Some(Statement::StructConstruct(tuple_construct)) = block.statements.pop() else {
133            panic!("Expected a tuple construct before the panic.");
134        };
135        // Assert that `err_data` bye the statement that was removed above.
136        assert_eq!(tuple_construct.output, err_data.var_id);
137
138        let panic_construct_statement = block.statements.pop();
139        // Assert that the output of `panic_construct_statement` is the first input of
140        // 'tuple_construct'.
141        assert_matches!(panic_construct_statement, Some(Statement::StructConstruct(panic_construct)) if panic_construct.output == tuple_construct.inputs[0].var_id);
142
143        if let Some(trace_fn) = opt_trace_fn {
144            block.statements.push(Statement::Call(StatementCall {
145                function: trace_fn,
146                inputs: vec![],
147                with_coupon: false,
148                outputs: vec![],
149                location: err_data.location,
150            }));
151        }
152
153        block.end = BlockEnd::Match {
154            info: MatchInfo::Extern(MatchExternInfo {
155                arms: vec![],
156                location: err_data.location,
157                function: panic_func_id,
158                inputs: vec![],
159            }),
160        }
161    }
162}
163
164/// Handles the lowering of panics in a single block.
165fn handle_block(
166    mut ctx: PanicLoweringContext<'_>,
167    mut block: Block,
168) -> Maybe<PanicLoweringContext<'_>> {
169    let mut block_ctx = PanicBlockLoweringContext { ctx, statements: Vec::new() };
170    for (i, stmt) in block.statements.iter().cloned().enumerate() {
171        if let Some((cur_block_end, continuation_block)) = block_ctx.handle_statement(&stmt)? {
172            // This case means that the lowering should split the block here.
173
174            // Block ended with a match.
175            ctx = block_ctx.handle_end(cur_block_end);
176            if let Some(continuation_block) = continuation_block {
177                // The rest of the statements in this block have not been handled yet, and should be
178                // handled as a part of the continuation block - the second block in the "split".
179                let block_to_edit =
180                    &mut ctx.block_queue[continuation_block.0 - ctx.flat_blocks.len()];
181                block_to_edit.statements.extend(block.statements.drain(i + 1..));
182                block_to_edit.end = block.end;
183            }
184            return Ok(ctx);
185        }
186    }
187    ctx = block_ctx.handle_end(block.end);
188    Ok(ctx)
189}
190
191pub struct PanicSignatureInfo {
192    /// The types of all the variables returned on OK: Reference variables and the original result.
193    ok_ret_tys: Vec<TypeId>,
194    /// The type of the Ok() variant.
195    ok_ty: TypeId,
196    /// The Ok() variant.
197    ok_variant: ConcreteVariant,
198    /// The Err() variant.
199    pub err_variant: ConcreteVariant,
200    /// The PanicResult concrete type - the new return type of the function.
201    pub actual_return_ty: TypeId,
202    /// Does the function always panic.
203    /// Note that if it does - the function returned type is always `(Panic, Array<felt252>)`.
204    pub always_panic: bool,
205}
206impl PanicSignatureInfo {
207    pub fn new(db: &dyn LoweringGroup, signature: &Signature) -> Self {
208        let extra_rets = signature.extra_rets.iter().map(|param| param.ty());
209        let original_return_ty = signature.return_type;
210
211        let ok_ret_tys = chain!(extra_rets, [original_return_ty]).collect_vec();
212        let ok_ty = semantic::TypeLongId::Tuple(ok_ret_tys.clone()).intern(db);
213        let ok_variant = get_core_enum_concrete_variant(
214            db,
215            "PanicResult",
216            vec![GenericArgumentId::Type(ok_ty)],
217            "Ok",
218        );
219        let err_variant = get_core_enum_concrete_variant(
220            db,
221            "PanicResult",
222            vec![GenericArgumentId::Type(ok_ty)],
223            "Err",
224        );
225        let always_panic = original_return_ty == never_ty(db);
226        let panic_ty = if always_panic { err_variant.ty } else { get_panic_ty(db, ok_ty) };
227
228        Self {
229            ok_ret_tys,
230            ok_ty,
231            ok_variant,
232            err_variant,
233            actual_return_ty: panic_ty,
234            always_panic,
235        }
236    }
237}
238
239struct PanicLoweringContext<'a> {
240    variables: VariableAllocator<'a>,
241    block_queue: VecDeque<Block>,
242    flat_blocks: BlocksBuilder,
243    panic_info: PanicSignatureInfo,
244}
245impl PanicLoweringContext<'_> {
246    pub fn db(&self) -> &dyn LoweringGroup {
247        self.variables.db
248    }
249
250    fn enqueue_block(&mut self, block: Block) -> BlockId {
251        self.block_queue.push_back(block);
252        BlockId(self.flat_blocks.len() + self.block_queue.len())
253    }
254}
255
256struct PanicBlockLoweringContext<'a> {
257    ctx: PanicLoweringContext<'a>,
258    statements: Vec<Statement>,
259}
260impl<'a> PanicBlockLoweringContext<'a> {
261    pub fn db(&self) -> &dyn LoweringGroup {
262        self.ctx.db()
263    }
264
265    fn new_var(&mut self, ty: TypeId, location: LocationId) -> VariableId {
266        self.ctx.variables.new_var(VarRequest { ty, location })
267    }
268
269    /// Handles a statement. If needed, returns the continuation block and the block end for this
270    /// block.
271    /// The continuation block happens when a panic match is added, and the block needs to be split.
272    /// The continuation block is the second block in the "split". This function already partially
273    /// creates this second block, and returns it.
274    /// In case there is no panic match - but just a panic, there is no continuation block.
275    fn handle_statement(&mut self, stmt: &Statement) -> Maybe<Option<(BlockEnd, Option<BlockId>)>> {
276        if let Statement::Call(call) = &stmt {
277            if let Some(with_body) = call.function.body(self.db())? {
278                if self.db().function_with_body_may_panic(with_body)? {
279                    return Ok(Some(self.handle_call_panic(call)?));
280                }
281            }
282        }
283        self.statements.push(stmt.clone());
284        Ok(None)
285    }
286
287    /// Handles a call statement to a panicking function.
288    /// Returns the continuation block ID for the caller to complete it, and the block end to set
289    /// for the current block.
290    fn handle_call_panic(&mut self, call: &StatementCall) -> Maybe<(BlockEnd, Option<BlockId>)> {
291        // Extract return variable.
292        let mut original_outputs = call.outputs.clone();
293        let location = call.location.with_auto_generation_note(self.db(), "Panic handling");
294
295        // Get callee info.
296        let callee_signature = call.function.signature(self.db())?;
297        let callee_info = PanicSignatureInfo::new(self.db(), &callee_signature);
298        if callee_info.always_panic {
299            // The panic value, which is actually of type (Panics, Array<felt252>).
300            let panic_result_var = self.new_var(callee_info.actual_return_ty, location);
301            // Emit the new statement.
302            self.statements.push(Statement::Call(StatementCall {
303                function: call.function,
304                inputs: call.inputs.clone(),
305                with_coupon: call.with_coupon,
306                outputs: vec![panic_result_var],
307                location,
308            }));
309            return Ok((BlockEnd::Panic(VarUsage { var_id: panic_result_var, location }), None));
310        }
311
312        // Allocate 2 new variables.
313        // panic_result_var - for the new return variable, with is actually of type PanicResult<ty>.
314        let panic_result_var = self.new_var(callee_info.actual_return_ty, location);
315        let n_callee_implicits = original_outputs.len() - callee_info.ok_ret_tys.len();
316        let mut call_outputs = original_outputs.drain(..n_callee_implicits).collect_vec();
317        call_outputs.push(panic_result_var);
318        // inner_ok_value - for the Ok() match arm input.
319        let inner_ok_value = self.new_var(callee_info.ok_ty, location);
320        // inner_ok_values - for the destructure.
321        let inner_ok_values = callee_info
322            .ok_ret_tys
323            .iter()
324            .copied()
325            .map(|ty| self.new_var(ty, location))
326            .collect_vec();
327
328        // Emit the new statement.
329        self.statements.push(Statement::Call(StatementCall {
330            function: call.function,
331            inputs: call.inputs.clone(),
332            with_coupon: call.with_coupon,
333            outputs: call_outputs,
334            location,
335        }));
336
337        // Start constructing a match on the result.
338        let block_continuation =
339            self.ctx.enqueue_block(Block { statements: vec![], end: BlockEnd::NotSet });
340
341        // Prepare Ok() match arm block. This block will be the continuation block.
342        // This block is only partially created. It is returned at this function to let the caller
343        // complete it.
344        let block_ok = self.ctx.enqueue_block(Block {
345            statements: vec![Statement::StructDestructure(StatementStructDestructure {
346                input: VarUsage { var_id: inner_ok_value, location },
347                outputs: inner_ok_values.clone(),
348            })],
349            end: BlockEnd::Goto(
350                block_continuation,
351                VarRemapping {
352                    remapping: zip_eq(
353                        original_outputs,
354                        inner_ok_values.into_iter().map(|var_id| VarUsage { var_id, location }),
355                    )
356                    .collect(),
357                },
358            ),
359        });
360
361        // Prepare Err() match arm block.
362        let err_var = self.new_var(self.ctx.panic_info.err_variant.ty, location);
363        let block_err = self.ctx.enqueue_block(Block {
364            statements: vec![],
365            end: BlockEnd::Panic(VarUsage { var_id: err_var, location }),
366        });
367
368        let cur_block_end = BlockEnd::Match {
369            info: MatchInfo::Enum(MatchEnumInfo {
370                concrete_enum_id: callee_info.ok_variant.concrete_enum_id,
371                input: VarUsage { var_id: panic_result_var, location },
372                arms: vec![
373                    MatchArm {
374                        arm_selector: MatchArmSelector::VariantId(callee_info.ok_variant),
375                        block_id: block_ok,
376                        var_ids: vec![inner_ok_value],
377                    },
378                    MatchArm {
379                        arm_selector: MatchArmSelector::VariantId(callee_info.err_variant),
380                        block_id: block_err,
381                        var_ids: vec![err_var],
382                    },
383                ],
384                location,
385            }),
386        };
387
388        Ok((cur_block_end, Some(block_continuation)))
389    }
390
391    fn handle_end(mut self, end: BlockEnd) -> PanicLoweringContext<'a> {
392        let end = match end {
393            BlockEnd::Goto(target, remapping) => BlockEnd::Goto(target, remapping),
394            BlockEnd::Panic(err_data) => {
395                // Wrap with PanicResult::Err.
396                let ty = self.ctx.panic_info.actual_return_ty;
397                let location = err_data.location;
398                let output = if self.ctx.panic_info.always_panic {
399                    err_data.var_id
400                } else {
401                    let output = self.new_var(ty, location);
402                    self.statements.push(Statement::EnumConstruct(StatementEnumConstruct {
403                        variant: self.ctx.panic_info.err_variant,
404                        input: err_data,
405                        output,
406                    }));
407                    output
408                };
409                BlockEnd::Return(vec![VarUsage { var_id: output, location }], location)
410            }
411            BlockEnd::Return(returns, location) => {
412                // Tuple construction.
413                let tupled_res = self.new_var(self.ctx.panic_info.ok_ty, location);
414                self.statements.push(Statement::StructConstruct(StatementStructConstruct {
415                    inputs: returns,
416                    output: tupled_res,
417                }));
418
419                // Wrap with PanicResult::Ok.
420                let ty = self.ctx.panic_info.actual_return_ty;
421                let output = self.new_var(ty, location);
422                self.statements.push(Statement::EnumConstruct(StatementEnumConstruct {
423                    variant: self.ctx.panic_info.ok_variant,
424                    input: VarUsage { var_id: tupled_res, location },
425                    output,
426                }));
427                BlockEnd::Return(vec![VarUsage { var_id: output, location }], location)
428            }
429            BlockEnd::NotSet => unreachable!(),
430            BlockEnd::Match { info } => BlockEnd::Match { info },
431        };
432        self.ctx.flat_blocks.alloc(Block { statements: self.statements, end });
433        self.ctx
434    }
435}
436
437// ============= Query implementations =============
438
439/// Query implementation of [crate::db::LoweringGroup::function_may_panic].
440pub fn function_may_panic(db: &dyn LoweringGroup, function: FunctionId) -> Maybe<bool> {
441    if let Some(body) = function.body(db)? {
442        return db.function_with_body_may_panic(body);
443    }
444    Ok(function.signature(db)?.panicable)
445}
446
447/// A trait to add helper methods in [LoweringGroup].
448pub trait MayPanicTrait<'a>: Upcast<dyn LoweringGroup + 'a> {
449    /// Returns whether a [ConcreteFunctionWithBodyId] may panic.
450    fn function_with_body_may_panic(&self, function: ConcreteFunctionWithBodyId) -> Maybe<bool> {
451        let scc_representative = self.upcast().lowered_scc_representative(
452            function,
453            DependencyType::Call,
454            LoweringStage::Monomorphized,
455        );
456        self.upcast().scc_may_panic(scc_representative)
457    }
458}
459impl<'a, T: Upcast<dyn LoweringGroup + 'a> + ?Sized> MayPanicTrait<'a> for T {}
460
461/// Query implementation of [crate::db::LoweringGroup::scc_may_panic].
462pub fn scc_may_panic(db: &dyn LoweringGroup, scc: ConcreteSCCRepresentative) -> Maybe<bool> {
463    // Find the SCC representative.
464    let scc_functions = db.lowered_scc(scc.0, DependencyType::Call, LoweringStage::Monomorphized);
465    for function in scc_functions {
466        if db.needs_withdraw_gas(function)? {
467            return Ok(true);
468        }
469        if db.has_direct_panic(function)? {
470            return Ok(true);
471        }
472        // For each direct callee, find if it may panic.
473        let direct_callees = db.lowered_direct_callees(
474            function,
475            DependencyType::Call,
476            LoweringStage::Monomorphized,
477        )?;
478        for direct_callee in direct_callees {
479            if let Some(callee_body) = direct_callee.body(db)? {
480                let callee_scc = db.lowered_scc_representative(
481                    callee_body,
482                    DependencyType::Call,
483                    LoweringStage::Monomorphized,
484                );
485                if callee_scc != scc && db.scc_may_panic(callee_scc)? {
486                    return Ok(true);
487                }
488            } else if direct_callee.signature(db)?.panicable {
489                return Ok(true);
490            }
491        }
492    }
493    Ok(false)
494}
495
496/// Query implementation of [crate::db::LoweringGroup::has_direct_panic].
497pub fn has_direct_panic(
498    db: &dyn LoweringGroup,
499    function_id: ConcreteFunctionWithBodyId,
500) -> Maybe<bool> {
501    let lowered_function = db.lowered_body(function_id, LoweringStage::Monomorphized)?;
502    Ok(itertools::any(&lowered_function.blocks, |(_, block)| {
503        matches!(&block.end, BlockEnd::Panic(..))
504    }))
505}