Skip to main content

cairo_lang_lowering/borrow_check/
mod.rs

1#[cfg(test)]
2#[path = "test.rs"]
3mod test;
4
5use cairo_lang_defs::ids::TraitFunctionId;
6use cairo_lang_diagnostics::{DiagnosticNote, Diagnostics};
7use cairo_lang_semantic::corelib::CorelibSemantic;
8use cairo_lang_semantic::items::functions::{GenericFunctionId, ImplGenericFunctionId};
9use cairo_lang_utils::Intern;
10use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
11use itertools::{Itertools, zip_eq};
12use salsa::Database;
13
14pub use self::demand::Demand;
15use self::demand::{AuxCombine, DemandReporter};
16use crate::analysis::{Analyzer, BackAnalysis, StatementLocation};
17use crate::diagnostic::LoweringDiagnosticKind::*;
18use crate::diagnostic::{LoweringDiagnostic, LoweringDiagnostics, LoweringDiagnosticsBuilder};
19use crate::ids::{FunctionId, LocationId, SemanticFunctionIdEx};
20use crate::{BlockId, Lowered, MatchInfo, Statement, VarRemapping, VarUsage, VariableId};
21
22pub mod demand;
23
24pub type BorrowCheckerDemand<'db> = Demand<VariableId, LocationId<'db>, PanicState>;
25pub struct BorrowChecker<'db, 'mt, 'r> {
26    db: &'db dyn Database,
27    diagnostics: &'mt mut LoweringDiagnostics<'db>,
28    lowered: &'r Lowered<'db>,
29    potential_destruct_calls: PotentialDestructCalls<'db>,
30    destruct_fn: TraitFunctionId<'db>,
31    panic_destruct_fn: TraitFunctionId<'db>,
32    is_panic_destruct_fn: bool,
33}
34
35/// A state saved for each position in the back analysis.
36/// Used to determine if this flow is guaranteed to end in a panic.
37#[derive(Copy, Clone, Default)]
38pub enum PanicState {
39    EndsWithPanic,
40    #[default]
41    Otherwise,
42}
43impl AuxCombine for PanicState {
44    fn merge<'a, I: Iterator<Item = &'a Self>>(mut iter: I) -> Self
45    where
46        Self: 'a,
47    {
48        if iter.all(|x| matches!(x, Self::EndsWithPanic)) {
49            Self::EndsWithPanic
50        } else {
51            Self::Otherwise
52        }
53    }
54}
55
56// Represents the item that caused the need for a drop.
57#[derive(Copy, Clone, Debug)]
58pub enum DropPosition<'db> {
59    // The trigger is a call to a panicable function.
60    Panic(LocationId<'db>),
61    // The trigger is a divergence in control flow.
62    Diverge(LocationId<'db>),
63}
64impl<'db> DropPosition<'db> {
65    fn enrich_as_notes(self, db: &'db dyn Database, notes: &mut Vec<DiagnosticNote<'db>>) {
66        let (text, location) = match self {
67            Self::Panic(location) => {
68                ("the variable needs to be dropped due to the potential panic here", location)
69            }
70            Self::Diverge(location) => {
71                ("the variable needs to be dropped due to the divergence here", location)
72            }
73        };
74        let location = location.long(db);
75        notes.push(DiagnosticNote::with_location(
76            text.into(),
77            location.stable_location.span_in_file(db),
78        ));
79        notes.extend(location.notes.iter().cloned());
80    }
81}
82
83impl<'db, 'mt> DemandReporter<VariableId, PanicState> for BorrowChecker<'db, 'mt, '_> {
84    // Note that for in BorrowChecker `IntroducePosition` is used to pass the cause of
85    // the drop.
86    type IntroducePosition = (Option<DropPosition<'db>>, BlockId);
87    type UsePosition = LocationId<'db>;
88
89    fn drop_aux(
90        &mut self,
91        (opt_drop_position, block_id): (Option<DropPosition<'db>>, BlockId),
92        var_id: VariableId,
93        panic_state: PanicState,
94    ) {
95        let var = &self.lowered.variables[var_id];
96        let Err(drop_err) = var.info.droppable.clone() else {
97            return;
98        };
99        let db = self.db;
100        let mut add_called_fn = |impl_id, function| {
101            self.potential_destruct_calls.entry(block_id).or_default().push(
102                cairo_lang_semantic::FunctionLongId {
103                    function: cairo_lang_semantic::ConcreteFunction {
104                        generic_function: GenericFunctionId::Impl(ImplGenericFunctionId {
105                            impl_id,
106                            function,
107                        }),
108                        generic_args: vec![],
109                    },
110                }
111                .intern(db)
112                .lowered(db),
113            );
114        };
115        let destruct_err = match var.info.destruct_impl.clone() {
116            Ok(impl_id) => {
117                add_called_fn(impl_id, self.destruct_fn);
118                return;
119            }
120            Err(err) => err,
121        };
122        let panic_destruct_err = if matches!(panic_state, PanicState::EndsWithPanic) {
123            match var.info.panic_destruct_impl.clone() {
124                Ok(impl_id) => {
125                    add_called_fn(impl_id, self.panic_destruct_fn);
126                    return;
127                }
128                Err(err) => Some(err),
129            }
130        } else {
131            None
132        };
133
134        let mut location = var.location.long(db).clone();
135        if let Some(drop_position) = opt_drop_position {
136            drop_position.enrich_as_notes(db, &mut location.notes);
137        }
138        self.diagnostics.report_by_location(
139            location
140                .with_note(DiagnosticNote::text_only(drop_err.format(db)))
141                .with_note(DiagnosticNote::text_only(destruct_err.format(db)))
142                .maybe_with_note(
143                    panic_destruct_err.map(|err| DiagnosticNote::text_only(err.format(db))),
144                ),
145            VariableNotDropped { drop_err, destruct_err },
146        );
147    }
148
149    fn dup(
150        &mut self,
151        position: LocationId<'db>,
152        var_id: VariableId,
153        next_usage_position: LocationId<'db>,
154    ) {
155        let var = &self.lowered.variables[var_id];
156        if let Err(inference_error) = var.info.copyable.clone() {
157            self.diagnostics.report_by_location(
158                next_usage_position
159                    .long(self.db)
160                    .clone()
161                    .add_note_with_location(self.db, "variable was previously used here", position)
162                    .with_note(DiagnosticNote::text_only(inference_error.format(self.db))),
163                VariableMoved { inference_error },
164            );
165        }
166    }
167}
168
169impl<'db, 'mt> Analyzer<'db, '_> for BorrowChecker<'db, 'mt, '_> {
170    type Info = BorrowCheckerDemand<'db>;
171
172    fn visit_stmt(
173        &mut self,
174        info: &mut Self::Info,
175        (block_id, _): StatementLocation,
176        stmt: &Statement<'db>,
177    ) {
178        info.variables_introduced(self, stmt.outputs(), (None, block_id));
179        match stmt {
180            Statement::Call(stmt) => {
181                if let Ok(signature) = stmt.function.signature(self.db)
182                    && signature.panicable
183                {
184                    // Be prepared to panic here.
185                    let panic_demand = BorrowCheckerDemand {
186                        aux: PanicState::EndsWithPanic,
187                        ..Default::default()
188                    };
189                    let location = (Some(DropPosition::Panic(stmt.location)), block_id);
190                    *info = BorrowCheckerDemand::merge_demands(
191                        &[(panic_demand, location), (info.clone(), location)],
192                        self,
193                    );
194                }
195            }
196            Statement::Desnap(stmt) => {
197                let var = &self.lowered.variables[stmt.output];
198                if let Err(inference_error) = var.info.copyable.clone() {
199                    self.diagnostics.report_by_location(
200                        var.location
201                            .long(self.db)
202                            .clone()
203                            .with_note(DiagnosticNote::text_only(inference_error.format(self.db))),
204                        DesnappingANonCopyableType { inference_error },
205                    );
206                }
207            }
208            _ => {}
209        }
210        info.variables_used(
211            self,
212            stmt.inputs().iter().map(|VarUsage { var_id, location }| (var_id, *location)),
213        );
214    }
215
216    fn visit_goto(
217        &mut self,
218        info: &mut Self::Info,
219        _statement_location: StatementLocation,
220        _target_block_id: BlockId,
221        remapping: &VarRemapping<'db>,
222    ) {
223        info.apply_remapping(
224            self,
225            remapping
226                .iter()
227                .map(|(dst, VarUsage { var_id: src, location })| (dst, (src, *location))),
228        );
229    }
230
231    fn merge_match(
232        &mut self,
233        (block_id, _): StatementLocation,
234        match_info: &MatchInfo<'db>,
235        infos: impl Iterator<Item = Self::Info>,
236    ) -> Self::Info {
237        let infos: Vec<_> = infos.collect();
238        let arm_demands = zip_eq(match_info.arms(), &infos)
239            .map(|(arm, demand)| {
240                let mut demand = demand.clone();
241                demand.variables_introduced(self, &arm.var_ids, (None, block_id));
242                (demand, (Some(DropPosition::Diverge(*match_info.location())), block_id))
243            })
244            .collect_vec();
245        let mut demand = BorrowCheckerDemand::merge_demands(&arm_demands, self);
246        demand.variables_used(
247            self,
248            match_info.inputs().iter().map(|VarUsage { var_id, location }| (var_id, *location)),
249        );
250        demand
251    }
252
253    fn info_from_return(
254        &mut self,
255        _statement_location: StatementLocation,
256        vars: &[VarUsage<'db>],
257    ) -> Self::Info {
258        let mut info = if self.is_panic_destruct_fn {
259            BorrowCheckerDemand { aux: PanicState::EndsWithPanic, ..Default::default() }
260        } else {
261            BorrowCheckerDemand::default()
262        };
263
264        info.variables_used(
265            self,
266            vars.iter().map(|VarUsage { var_id, location }| (var_id, *location)),
267        );
268        info
269    }
270
271    fn info_from_panic(
272        &mut self,
273        _statement_location: StatementLocation,
274        data: &VarUsage<'db>,
275    ) -> Self::Info {
276        let mut info = BorrowCheckerDemand { aux: PanicState::EndsWithPanic, ..Default::default() };
277        info.variables_used(self, std::iter::once((&data.var_id, data.location)));
278        info
279    }
280}
281
282/// The possible destruct calls per block.
283pub type PotentialDestructCalls<'db> = UnorderedHashMap<BlockId, Vec<FunctionId<'db>>>;
284
285/// The borrow checker result.
286#[derive(Eq, PartialEq, Debug, Default, salsa::Update)]
287pub struct BorrowCheckResult<'db> {
288    /// The possible destruct calls per block.
289    pub block_extra_calls: PotentialDestructCalls<'db>,
290    /// The diagnostics generated during borrow checking.
291    pub diagnostics: Diagnostics<'db, LoweringDiagnostic<'db>>,
292}
293
294/// Report borrow checking diagnostics.
295/// Returns the potential destruct function calls per block.
296pub fn borrow_check<'db>(
297    db: &'db dyn Database,
298    is_panic_destruct_fn: bool,
299    lowered: &'db Lowered<'db>,
300) -> BorrowCheckResult<'db> {
301    if lowered.blocks.has_root().is_err() {
302        return Default::default();
303    }
304    let mut diagnostics = LoweringDiagnostics::default();
305    let info = db.core_info();
306    let destruct_fn = info.destruct_fn;
307    let panic_destruct_fn = info.panic_destruct_fn;
308
309    let checker = BorrowChecker {
310        db,
311        diagnostics: &mut diagnostics,
312        lowered,
313        potential_destruct_calls: Default::default(),
314        destruct_fn,
315        panic_destruct_fn,
316        is_panic_destruct_fn,
317    };
318    let mut analysis = BackAnalysis::new(lowered, checker);
319    let mut root_demand = analysis.get_root_info();
320    root_demand.variables_introduced(
321        &mut analysis.analyzer,
322        &lowered.parameters,
323        (None, BlockId::root()),
324    );
325    let block_extra_calls = analysis.analyzer.potential_destruct_calls;
326
327    let finalize_res = root_demand.finalize();
328    // If there are errors in the lowering phase, there may be undefined variables (e.g., due to
329    // using a moved variable). Skip the following assert in that case.
330    if !lowered.diagnostics.has_errors() {
331        assert!(finalize_res, "Undefined variable should not happen at this stage");
332    }
333
334    BorrowCheckResult { block_extra_calls, diagnostics: diagnostics.build() }
335}
336
337/// Borrow check the params of the function are panic destruct, as this function may have a gas
338/// withdrawal.
339pub fn borrow_check_possible_withdraw_gas<'db>(
340    db: &'db dyn Database,
341    location_id: LocationId<'db>,
342    lowered: &Lowered<'db>,
343    diagnostics: &mut LoweringDiagnostics<'db>,
344) {
345    let info = db.core_info();
346    let destruct_fn = info.destruct_fn;
347    let panic_destruct_fn = info.panic_destruct_fn;
348    let mut checker = BorrowChecker {
349        db,
350        diagnostics,
351        lowered,
352        potential_destruct_calls: Default::default(),
353        destruct_fn,
354        panic_destruct_fn,
355        is_panic_destruct_fn: false,
356    };
357    let position = (
358        Some(DropPosition::Panic(location_id.with_auto_generation_note(db, "withdraw_gas"))),
359        BlockId::root(),
360    );
361    for param in &lowered.parameters {
362        checker.drop_aux(position, *param, PanicState::EndsWithPanic);
363    }
364}