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