Skip to main content

cairo_lang_lowering/optimizations/
early_unsafe_panic.rs

1#[cfg(test)]
2#[path = "early_unsafe_panic_test.rs"]
3mod test;
4
5use cairo_lang_defs::ids::ExternFunctionId;
6use cairo_lang_filesystem::flag::FlagsGroup;
7use cairo_lang_semantic::helper::ModuleHelper;
8use cairo_lang_utils::unordered_hash_set::UnorderedHashSet;
9use salsa::Database;
10
11use crate::analysis::core::StatementLocation;
12use crate::analysis::{DataflowAnalyzer, DataflowBackAnalysis, Direction, Edge};
13use crate::ids::{LocationId, SemanticFunctionIdEx};
14use crate::{
15    Block, BlockEnd, BlockId, Lowered, MatchExternInfo, MatchInfo, Statement, StatementCall,
16};
17
18/// Adds an early unsafe_panic when we detect that `return` is unreachable from a certain point in
19/// the code. This step is needed to avoid issues with undroppable references in Sierra to CASM.
20///
21/// This step might replace a match on an empty enum with a call to unsafe_panic and we rely on the
22/// 'trim_unreachable' optimization to clean that up.
23pub fn early_unsafe_panic<'db>(db: &'db dyn Database, lowered: &mut Lowered<'db>) {
24    if !db.flag_unsafe_panic() || lowered.blocks.is_empty() {
25        return;
26    }
27
28    let core = ModuleHelper::core(db);
29    let libfuncs_with_sideffect = UnorderedHashSet::from_iter([
30        core.submodule("debug").extern_function_id("print"),
31        core.submodule("internal").extern_function_id("trace"),
32    ]);
33
34    let mut ctx = UnsafePanicContext { db, libfuncs_with_sideffect, fixes: Vec::new() };
35    let root_info = DataflowBackAnalysis::new(lowered, &mut ctx).run();
36
37    // If the root block is completely unreachable (no path to return), replace entire function
38    // with unsafe_panic from the start.
39    let fixes = if let ReachableSideEffects::Unreachable(location) = root_info {
40        vec![((BlockId::root(), 0), location)]
41    } else {
42        ctx.fixes
43    };
44
45    let panic_func_id = core.submodule("panics").function_id("unsafe_panic", vec![]).lowered(db);
46    for ((block_id, statement_idx), location) in fixes {
47        let block = &mut lowered.blocks[block_id];
48        block.statements.truncate(statement_idx);
49
50        block.end = BlockEnd::Match {
51            info: MatchInfo::Extern(MatchExternInfo {
52                arms: vec![],
53                location,
54                function: panic_func_id,
55                inputs: vec![],
56            }),
57        }
58    }
59}
60
61pub struct UnsafePanicContext<'db> {
62    db: &'db dyn Database,
63
64    /// The list of blocks where we can insert unsafe_panic.
65    fixes: Vec<(StatementLocation, LocationId<'db>)>,
66
67    /// libfuncs with side effects that we need to ignore.
68    libfuncs_with_sideffect: UnorderedHashSet<ExternFunctionId<'db>>,
69}
70
71impl<'db> UnsafePanicContext<'db> {
72    /// Returns true if the statement has side effects.
73    pub fn has_side_effects(&self, stmt: &Statement<'db>) -> bool {
74        if let Statement::Call(StatementCall { function, .. }) = stmt {
75            let Some((extern_fn, _gargs)) = function.get_extern(self.db) else {
76                return false;
77            };
78
79            if self.libfuncs_with_sideffect.contains(&extern_fn) {
80                return true;
81            }
82        }
83
84        false
85    }
86}
87
88/// Can this state lead to a return or a statement with side effect.
89#[derive(Clone, Default, PartialEq, Debug)]
90pub enum ReachableSideEffects<'db> {
91    /// Some return statement or statement with side effect is reachable.
92    #[default]
93    Reachable,
94    /// No return statement or statement with side effect is reachable.
95    /// holds the location of the closest match with no returning arms.
96    Unreachable(LocationId<'db>),
97}
98
99impl<'db, 'a> DataflowAnalyzer<'db, 'a> for UnsafePanicContext<'db> {
100    type Info = ReachableSideEffects<'db>;
101    const DIRECTION: Direction = Direction::Backward;
102
103    fn transfer_block(&mut self, info: &mut Self::Info, block_id: BlockId, block: &'a Block<'db>) {
104        if let BlockEnd::Match { info: match_info } = &block.end
105            && let ReachableSideEffects::Unreachable(_) = info
106        {
107            self.fixes.push(((block_id, block.statements.len()), *match_info.location()));
108        }
109        let ReachableSideEffects::Unreachable(location) = *info else {
110            return;
111        };
112        // Everything past the last side-effecting statement is dead (no `return` is reachable
113        // from here), so replace it with an early panic — but keep the side effects themselves.
114        for (i, stmt) in block.statements.iter().enumerate().rev() {
115            if self.has_side_effects(stmt) {
116                self.fixes.push(((block_id, i + 1), location));
117                *info = ReachableSideEffects::Reachable;
118                break;
119            }
120        }
121    }
122
123    fn merge(
124        &mut self,
125        lowered: &Lowered<'db>,
126        statement_location: StatementLocation,
127        info1: Self::Info,
128        info2: Self::Info,
129    ) -> Self::Info {
130        match (info1, info2) {
131            (ReachableSideEffects::Reachable, _) | (_, ReachableSideEffects::Reachable) => {
132                ReachableSideEffects::Reachable
133            }
134            // Both are unreachable.
135            (ReachableSideEffects::Unreachable(_), ReachableSideEffects::Unreachable(_)) => {
136                ReachableSideEffects::Unreachable(
137                    lowered.blocks[statement_location.0].end.location().unwrap(),
138                )
139            }
140        }
141    }
142
143    fn transfer_edge(&mut self, info: &Self::Info, edge: &Edge<'db, 'a>) -> Self::Info {
144        if let Edge::MatchArm { arm, .. } = edge
145            && let ReachableSideEffects::Unreachable(l) = info
146        {
147            self.fixes.push(((arm.block_id, 0), *l));
148        }
149        info.clone()
150    }
151
152    fn initial_info(&mut self, _block_id: BlockId, block_end: &'a BlockEnd<'db>) -> Self::Info {
153        match block_end {
154            BlockEnd::Match { info } => ReachableSideEffects::Unreachable(*info.location()),
155            _ => ReachableSideEffects::Reachable,
156        }
157    }
158}