Skip to main content

cairo_lang_lowering/optimizations/
reboxing.rs

1#[cfg(test)]
2#[path = "reboxing_test.rs"]
3mod reboxing_test;
4
5use std::rc::Rc;
6
7use cairo_lang_diagnostics::Maybe;
8use cairo_lang_semantic::corelib::core_box_ty;
9use cairo_lang_utils::ordered_hash_map::{Entry, OrderedHashMap};
10use cairo_lang_utils::ordered_hash_set::OrderedHashSet;
11use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
12use itertools::Itertools;
13use salsa::Database;
14
15use super::var_renamer::VarRenamer;
16use crate::analysis::StatementLocation;
17use crate::blocks::Blocks;
18use crate::utils::RebuilderEx;
19use crate::{
20    BlockEnd, Lowered, Statement, StatementStructDestructure, VarUsage, Variable, VariableArena,
21    VariableId,
22};
23
24/// Data for the MemberOfUnboxed variant.
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub struct MemberOfUnboxedData {
27    /// The source reboxing value.
28    pub source: Rc<ReboxingValue>,
29    /// The member index.
30    pub member: usize,
31    /// The location where the deconstruct statement occurs.
32    pub deconstruct_location: StatementLocation,
33}
34
35/// The possible values for the reboxing analysis.
36#[derive(Debug, Clone, PartialEq, Eq, Hash)]
37pub enum ReboxingValue {
38    /// No reboxing can be done. Relevant after a meet of two paths.
39    Revoked,
40    /// The variable is unboxed from a different variable.
41    Unboxed(VariableId),
42    /// The variable is a member of an unboxed variable.
43    MemberOfUnboxed(MemberOfUnboxedData),
44}
45
46/// Represents a candidate for reboxing optimization.
47#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48pub struct ReboxCandidate {
49    /// The reboxing data.
50    pub source: ReboxingValue,
51    /// The reboxed variable (output of into_box).
52    pub reboxed_var: VariableId,
53    /// Location where into_box call occurs (block_id, stmt_idx).
54    pub into_box_location: StatementLocation,
55}
56
57/// Finds reboxing candidates in the lowered function. Assumes a topological sort of blocks.
58///
59/// This analysis detects patterns where we:
60/// 1. Unbox a struct
61/// 2. (Optional) Destructure it
62/// 3. Box one of the members back
63///
64/// Returns candidates that can be optimized with struct_boxed_deconstruct libfunc calls.
65pub fn find_reboxing_candidates<'db>(lowered: &Lowered<'db>) -> OrderedHashSet<ReboxCandidate> {
66    if lowered.blocks.is_empty() {
67        return Default::default();
68    }
69
70    trace!("Running reboxing analysis...");
71
72    // TODO(eytan-starkware): When applied, reboxing analysis should replace the existing
73    // deconstruct with a boxed-deconstruct, and add unbox statements on members as needed.
74
75    // TODO(eytan-starkware): Support "snapshot" equality tracking in the reboxing analysis.
76    // Currently we track unboxed values and their members, but we don't properly handle
77    // the case where snapshots are taken and we need to track that a snapshot of a member
78    // is equivalent to a member of a snapshot.
79
80    let mut current_state: OrderedHashMap<VariableId, ReboxingValue> = Default::default();
81    let mut candidates: OrderedHashSet<ReboxCandidate> = Default::default();
82
83    for (block_id, block) in lowered.blocks.iter() {
84        for (stmt_idx, stmt) in block.statements.iter().enumerate() {
85            match stmt {
86                Statement::Unbox(unbox_stmt) => {
87                    let res = ReboxingValue::Unboxed(unbox_stmt.input.var_id);
88                    current_state.insert(unbox_stmt.output, res);
89                }
90                Statement::IntoBox(into_box_stmt) => {
91                    let source = current_state
92                        .get(&into_box_stmt.input.var_id)
93                        .unwrap_or(&ReboxingValue::Revoked);
94                    if !matches!(source, ReboxingValue::Revoked) {
95                        candidates.insert(ReboxCandidate {
96                            source: source.clone(),
97                            reboxed_var: into_box_stmt.output,
98                            into_box_location: (block_id, stmt_idx),
99                        });
100                    }
101                }
102                Statement::StructDestructure(destructure_stmt) => {
103                    let info = &lowered.variables[destructure_stmt.input.var_id].info;
104                    if info.copyable.is_err() || info.droppable.is_err() {
105                        continue;
106                    }
107                    let input_state = current_state
108                        .get(&destructure_stmt.input.var_id)
109                        .cloned()
110                        .unwrap_or(ReboxingValue::Revoked);
111                    match input_state {
112                        ReboxingValue::Revoked => {}
113                        ReboxingValue::MemberOfUnboxed { .. } | ReboxingValue::Unboxed(_) => {
114                            let input_state_rc = Rc::new(input_state);
115                            for (member_idx, output_var) in
116                                destructure_stmt.outputs.iter().enumerate()
117                            {
118                                let res = ReboxingValue::MemberOfUnboxed(MemberOfUnboxedData {
119                                    source: Rc::clone(&input_state_rc),
120                                    deconstruct_location: (block_id, stmt_idx),
121                                    member: member_idx,
122                                });
123
124                                current_state.insert(*output_var, res);
125                            }
126                        }
127                    }
128                }
129                _ => {}
130            }
131        }
132
133        // Process block end to handle variable remapping
134        if let BlockEnd::Goto(_, remapping) = &block.end {
135            for (dst, src_usage) in remapping.iter() {
136                let src_state =
137                    current_state.get(&src_usage.var_id).cloned().unwrap_or(ReboxingValue::Revoked);
138                update_reboxing_variable_join(&mut current_state, *dst, src_state);
139            }
140        }
141    }
142
143    trace!("Found {} reboxing candidate(s).", candidates.len());
144    candidates
145}
146
147/// Update the reboxing state for a variable join. If the variable is already in the state with a
148/// different value, it is revoked.
149fn update_reboxing_variable_join(
150    current_state: &mut OrderedHashMap<id_arena::Id<crate::VariableMarker>, ReboxingValue>,
151    var: VariableId,
152    res: ReboxingValue,
153) {
154    match current_state.entry(var) {
155        Entry::Vacant(entry) => {
156            entry.insert(res);
157        }
158        Entry::Occupied(mut entry) => {
159            if entry.get() != &res {
160                entry.insert(ReboxingValue::Revoked);
161            }
162        }
163    }
164}
165
166/// Represents an operation to apply to the lowered function.
167#[derive(Debug)]
168enum ReboxingOperation<'db> {
169    Add { location: StatementLocation, statement: Statement<'db> },
170    Remove { location: StatementLocation },
171}
172
173impl<'db> ReboxingOperation<'db> {
174    /// Applies this operation to the lowered function, adding or removing statements as specified.
175    /// Note: This function modifies the lowered object, and applying multiple operations may
176    /// require applying in reverse order to statement location.
177    fn apply(self, lowered: &mut Lowered<'db>) {
178        match self {
179            ReboxingOperation::Add { location: (block_id, stmt_idx), statement } => {
180                lowered.blocks[block_id].statements.insert(stmt_idx, statement);
181            }
182            ReboxingOperation::Remove { location: (block_id, stmt_idx) } => {
183                lowered.blocks[block_id].statements.remove(stmt_idx);
184            }
185        }
186    }
187
188    /// Returns the statement location associated with this operation.
189    fn location(&self) -> StatementLocation {
190        match self {
191            ReboxingOperation::Add { location, .. } => *location,
192            ReboxingOperation::Remove { location } => *location,
193        }
194    }
195}
196
197/// Applies reboxing optimizations to the lowered function using the provided candidates.
198pub fn apply_reboxing_candidates<'db>(
199    db: &'db dyn Database,
200    lowered: &mut Lowered<'db>,
201    candidates: &OrderedHashSet<ReboxCandidate>,
202) -> Maybe<()> {
203    if candidates.is_empty() {
204        trace!("No reboxing candidates to apply.");
205        return Ok(());
206    }
207
208    trace!("Applying {} reboxing optimization(s).", candidates.len());
209
210    let mut renamer = VarRenamer::default();
211    let mut operations = Vec::new();
212    let mut added_boxes = UnorderedHashMap::default();
213
214    for candidate in candidates.iter() {
215        let box_var_to_use = match &candidate.source {
216            ReboxingValue::Revoked => unreachable!("Revoked reboxing candidate should not exist"),
217            ReboxingValue::Unboxed(id) => *id,
218            ReboxingValue::MemberOfUnboxed(data) => {
219                // Create output variables for all members (all will be Box<MemberType>), or get
220                // existing ones.
221                create_deconstruct_statements(
222                    db,
223                    &mut lowered.variables,
224                    &lowered.blocks,
225                    &mut operations,
226                    &mut added_boxes,
227                    data,
228                )
229            }
230        };
231        renamer.renamed_vars.insert(candidate.reboxed_var, box_var_to_use);
232        operations.push(ReboxingOperation::Remove { location: candidate.into_box_location });
233    }
234
235    // Sort operations by location in reverse order (highest block_id, highest stmt_idx first)
236    // This ensures that an operation index won't change before it is updated.
237    operations.sort_by_key(|op| {
238        let (block_id, stmt_idx) = op.location();
239        (std::cmp::Reverse(block_id.0), std::cmp::Reverse(stmt_idx))
240    });
241
242    operations.into_iter().for_each(|operation| operation.apply(lowered));
243
244    for block in lowered.blocks.iter_mut() {
245        *block = renamer.rebuild_block(block);
246    }
247    Ok(())
248}
249
250/// Recursively allocate boxed variables for a deconstruct statement and its original deconstruct
251/// creation. Add the boxed_deconstruct operations (recursively) to the operations vector.
252#[allow(clippy::too_many_arguments)]
253fn create_deconstruct_statements<'db>(
254    db: &'db dyn Database,
255    variables: &mut VariableArena<'db>,
256    blocks: &Blocks<'db>,
257    operations: &mut Vec<ReboxingOperation<'db>>,
258    added_boxes: &mut UnorderedHashMap<VariableId, Vec<VariableId>>,
259    data: &MemberOfUnboxedData,
260) -> VariableId {
261    let deconstruct_statement = &blocks[data.deconstruct_location];
262    let deconstruct_outputs = deconstruct_statement.outputs();
263    let input_var = deconstruct_statement.inputs()[0];
264    if let Some(boxed_vars) = added_boxes.get(&input_var.var_id) {
265        return boxed_vars[data.member];
266    }
267
268    let source_box = match data.source.as_ref() {
269        ReboxingValue::Revoked => {
270            unreachable!("A Revoked reboxing state should be blocked before.")
271        }
272        ReboxingValue::Unboxed(id) => {
273            // We are at top of the recursion and found our source var.
274            *id
275        }
276        ReboxingValue::MemberOfUnboxed(data) => {
277            // Recurse to find the source var.
278            create_deconstruct_statements(db, variables, blocks, operations, added_boxes, data)
279        }
280    };
281    let outputs = deconstruct_outputs
282        .iter()
283        .map(|out_var_id| {
284            let out_var = &variables[*out_var_id];
285            let box_ty = core_box_ty(db, out_var.ty);
286            let out_location = out_var.location;
287            variables.alloc(Variable::with_default_context(db, box_ty, out_location))
288        })
289        .collect_vec();
290
291    operations.push(ReboxingOperation::Add {
292        location: data.deconstruct_location,
293        statement: Statement::StructDestructure(StatementStructDestructure {
294            input: VarUsage { var_id: source_box, location: input_var.location },
295            outputs: outputs.clone(),
296        }),
297    });
298    added_boxes.insert(input_var.var_id, outputs);
299    added_boxes[&input_var.var_id][data.member]
300}
301
302/// Applies the reboxing optimization to the lowered function.
303///
304/// This optimization detects patterns where we:
305/// 1. Unbox a struct
306/// 2. (Optional) Destructure it
307/// 3. Box one of the members back
308///
309/// And replaces it with a direct struct_boxed_deconstruct libfunc call.
310pub fn apply_reboxing<'db>(db: &'db dyn Database, lowered: &mut Lowered<'db>) -> Maybe<()> {
311    let candidates = find_reboxing_candidates(lowered);
312    apply_reboxing_candidates(db, lowered, &candidates)
313}