Skip to content

Commit cc5f6b4

Browse files
(optimization): automatically detecting reboxing of struct member and applying struct_box_deconstruct libfunc
1 parent 443f98c commit cc5f6b4

File tree

9 files changed

+1282
-2
lines changed

9 files changed

+1282
-2
lines changed

crates/cairo-lang-filesystem/src/flag.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,20 @@ pub enum Flag {
2020
///
2121
/// Default is false as it makes panic unprovable.
2222
UnsafePanic(bool),
23+
/// Whether to use future_sierra in the generated code.
24+
///
25+
/// Default is false.
26+
FutureSierra(bool),
2327
}
2428

2529
/// Returns the value of the `unsafe_panic` flag, or `false` if the flag is not set.
2630
pub fn flag_unsafe_panic(db: &dyn salsa::Database) -> bool {
2731
let flag = FlagId::new(db, FlagLongId("unsafe_panic".into()));
2832
if let Some(flag) = db.get_flag(flag) { *flag == Flag::UnsafePanic(true) } else { false }
2933
}
34+
35+
/// Returns the value of the `future_sierra` flag, or `false` if the flag is not set.
36+
pub fn flag_future_sierra(db: &dyn salsa::Database) -> bool {
37+
let flag = FlagId::new(db, FlagLongId("future_sierra".into()));
38+
if let Some(flag) = db.get_flag(flag) { *flag == Flag::FutureSierra(true) } else { false }
39+
}

crates/cairo-lang-lowering/src/optimizations/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub mod dedup_blocks;
2727
pub mod early_unsafe_panic;
2828
pub mod gas_redeposit;
2929
pub mod match_optimizer;
30+
pub mod reboxing;
3031
pub mod remappings;
3132
pub mod reorder_statements;
3233
pub mod return_optimization;
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
#[cfg(test)]
2+
#[path = "reboxing_test.rs"]
3+
mod reboxing_test;
4+
5+
use std::rc::Rc;
6+
7+
use cairo_lang_filesystem::flag::flag_future_sierra;
8+
use cairo_lang_semantic::helper::ModuleHelper;
9+
use cairo_lang_semantic::items::structure::StructSemantic;
10+
use cairo_lang_semantic::types::{TypesSemantic, peel_snapshots};
11+
use cairo_lang_semantic::{ConcreteTypeId, GenericArgumentId, TypeId, TypeLongId};
12+
use cairo_lang_utils::ordered_hash_map::{Entry, OrderedHashMap};
13+
use cairo_lang_utils::ordered_hash_set::OrderedHashSet;
14+
use salsa::Database;
15+
16+
use crate::borrow_check::analysis::StatementLocation;
17+
use crate::{
18+
BlockEnd, Lowered, Statement, StatementStructDestructure, VarUsage, Variable, VariableArena,
19+
VariableId,
20+
};
21+
22+
/// The possible values for the reboxing analysis.
23+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
24+
pub enum ReboxingValue {
25+
/// No reboxing can be done. Relevant after a meet of two paths.
26+
Revoked,
27+
/// The variable is unboxed from a different variable.
28+
Unboxed(VariableId),
29+
/// The variable is a member of an unboxed variable.
30+
MemberOfUnboxed { source: Rc<ReboxingValue>, member: usize },
31+
}
32+
33+
/// Represents a candidate for reboxing optimization.
34+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35+
pub struct ReboxCandidate {
36+
/// The reboxing data
37+
pub source: ReboxingValue,
38+
/// The reboxed variable (output of into_box)
39+
pub reboxed_var: VariableId,
40+
/// Location where into_box call occurs (block_id, stmt_idx)
41+
pub into_box_location: StatementLocation,
42+
}
43+
44+
/// Finds reboxing candidates in the lowered function. Assumes a topological sort of blocks.
45+
///
46+
/// This analysis detects patterns where we:
47+
/// 1. Unbox a struct
48+
/// 2. (Optional) Destructure it
49+
/// 3. Box one of the members back
50+
///
51+
/// Returns candidates that can be optimized with struct_boxed_deconstruct libfunc calls.
52+
pub fn find_reboxing_candidates<'db>(
53+
db: &'db dyn Database,
54+
lowered: &Lowered<'db>,
55+
) -> OrderedHashSet<ReboxCandidate> {
56+
if lowered.blocks.is_empty() {
57+
return OrderedHashSet::default();
58+
}
59+
60+
trace!("Running reboxing analysis...");
61+
62+
let core = ModuleHelper::core(db);
63+
let box_module = core.submodule("box");
64+
let unbox_id = box_module.extern_function_id("unbox");
65+
let into_box_id = box_module.extern_function_id("into_box");
66+
67+
// TODO(eytan-starkware): When applied, reboxing analysis should replace the existing
68+
// deconstruct with a boxed-deconstruct, and add unbox statements on members as needed.
69+
70+
// TODO(eytan-starkware): Support "snapshot" equality tracking in the reboxing analysis.
71+
// Currently we track unboxed values and their members, but we don't properly handle
72+
// the case where snapshots are taken and we need to track that a snapshot of a member
73+
// is equivalent to a member of a snapshot.
74+
75+
let mut current_state: OrderedHashMap<VariableId, ReboxingValue> = Default::default();
76+
let mut candidates: OrderedHashSet<ReboxCandidate> = Default::default();
77+
78+
for (block_id, block) in lowered.blocks.iter() {
79+
// Process statements
80+
for (stmt_idx, stmt) in block.statements.iter().enumerate() {
81+
match stmt {
82+
Statement::Call(call_stmt) => {
83+
if let Some((extern_id, _)) = call_stmt.function.get_extern(db) {
84+
if extern_id == unbox_id {
85+
let res = ReboxingValue::Unboxed(call_stmt.inputs[0].var_id);
86+
current_state.insert(call_stmt.outputs[0], res);
87+
} else if extern_id == into_box_id {
88+
let source = current_state
89+
.get(&call_stmt.inputs[0].var_id)
90+
.unwrap_or(&ReboxingValue::Revoked);
91+
if matches!(source, ReboxingValue::Revoked) {
92+
continue;
93+
}
94+
candidates.insert(ReboxCandidate {
95+
source: source.clone(),
96+
reboxed_var: call_stmt.outputs[0],
97+
into_box_location: (block_id, stmt_idx),
98+
});
99+
}
100+
}
101+
}
102+
Statement::StructDestructure(destructure_stmt) => {
103+
let input_state = current_state
104+
.get(&destructure_stmt.input.var_id)
105+
.cloned()
106+
.unwrap_or(ReboxingValue::Revoked);
107+
match input_state {
108+
ReboxingValue::Revoked => {}
109+
ReboxingValue::MemberOfUnboxed { .. } | ReboxingValue::Unboxed(_) => {
110+
for (member_idx, output_var) in
111+
destructure_stmt.outputs.iter().enumerate()
112+
{
113+
let res = ReboxingValue::MemberOfUnboxed {
114+
source: Rc::new(input_state.clone()),
115+
member: member_idx,
116+
};
117+
118+
current_state.insert(*output_var, res);
119+
}
120+
}
121+
}
122+
}
123+
_ => {}
124+
}
125+
}
126+
127+
// Process block end to handle variable remapping
128+
if let BlockEnd::Goto(_, remapping) = &block.end {
129+
for (dst, src_usage) in remapping.iter() {
130+
let src_state =
131+
current_state.get(&src_usage.var_id).cloned().unwrap_or(ReboxingValue::Revoked);
132+
update_reboxing_variable_join(&mut current_state, *dst, src_state);
133+
}
134+
}
135+
}
136+
137+
trace!("Found {} reboxing candidate(s).", candidates.len());
138+
candidates
139+
}
140+
141+
/// Update the reboxing state for a variable join. If the variable is already in the state with a
142+
/// different value, it is revoked.
143+
fn update_reboxing_variable_join(
144+
current_state: &mut OrderedHashMap<id_arena::Id<crate::VariableMarker>, ReboxingValue>,
145+
var: VariableId,
146+
res: ReboxingValue,
147+
) {
148+
match current_state.entry(var) {
149+
Entry::Vacant(entry) => {
150+
entry.insert(res);
151+
}
152+
Entry::Occupied(mut entry) => {
153+
if entry.get() != &res {
154+
entry.insert(ReboxingValue::Revoked);
155+
}
156+
}
157+
}
158+
}
159+
160+
/// Applies reboxing optimizations to the lowered function using the provided candidates.
161+
pub fn apply_reboxing_candidates<'db>(
162+
db: &'db dyn Database,
163+
lowered: &mut Lowered<'db>,
164+
candidates: &OrderedHashSet<ReboxCandidate>,
165+
) {
166+
if candidates.is_empty() {
167+
trace!("No reboxing candidates to apply.");
168+
return;
169+
}
170+
171+
trace!("Applying {} reboxing optimization(s).", candidates.len());
172+
173+
for candidate in candidates {
174+
apply_reboxing_candidate(db, lowered, candidate);
175+
}
176+
}
177+
178+
/// Applies the reboxing optimization to the lowered function.
179+
///
180+
/// This optimization detects patterns where we:
181+
/// 1. Unbox a struct
182+
/// 2. (Optional) Destructure it
183+
/// 3. Box one of the members back
184+
///
185+
/// And replaces it with a direct struct_boxed_deconstruct libfunc call.
186+
pub fn apply_reboxing<'db>(db: &'db dyn Database, lowered: &mut Lowered<'db>) {
187+
if flag_future_sierra(db) {
188+
let candidates = find_reboxing_candidates(db, lowered);
189+
apply_reboxing_candidates(db, lowered, &candidates);
190+
}
191+
}
192+
193+
/// Applies a single reboxing optimization for the given candidate.
194+
fn apply_reboxing_candidate<'db>(
195+
db: &'db dyn Database,
196+
lowered: &mut Lowered<'db>,
197+
candidate: &ReboxCandidate,
198+
) {
199+
trace!(
200+
"Applying optimization: candidate={:?}, reboxed={}",
201+
candidate.source,
202+
candidate.reboxed_var.index()
203+
);
204+
205+
// TODO(eytan-starkware): Handle snapshot of box (e.g., @Box<T>).
206+
// Only support MemberOfUnboxed where source is Unboxed for now.
207+
let ReboxingValue::MemberOfUnboxed { source, member } = &candidate.source else {
208+
// If source is not member of unboxed, we are reboxing original value which is not supported
209+
// yet.
210+
return;
211+
};
212+
let ReboxingValue::Unboxed(source_var) = **source else {
213+
// When source of the value is not `Unboxes`, it is a nested MemberOfUnboxed, which is not
214+
// supported yet.
215+
return;
216+
};
217+
// Create the struct_boxed_deconstruct call
218+
if let Some(new_stmt) = create_struct_boxed_deconstruct_call(
219+
db,
220+
&mut lowered.variables,
221+
source_var,
222+
*member,
223+
candidate.reboxed_var,
224+
&lowered.blocks[candidate.into_box_location.0].statements[candidate.into_box_location.1],
225+
) {
226+
// swap to the new call
227+
let (into_box_block, into_box_stmt_idx) = candidate.into_box_location;
228+
lowered.blocks[into_box_block].statements[into_box_stmt_idx] = new_stmt;
229+
230+
trace!("Successfully applied reboxing optimization.");
231+
}
232+
}
233+
234+
/// Creates a struct_boxed_deconstruct call statement.
235+
/// Returns None if the call cannot be created.
236+
fn create_struct_boxed_deconstruct_call<'db>(
237+
db: &'db dyn Database,
238+
variables: &mut VariableArena<'db>,
239+
boxed_struct_var: VariableId,
240+
member_index: usize,
241+
output_var: VariableId,
242+
old_stmt: &Statement<'db>,
243+
) -> Option<Statement<'db>> {
244+
// TODO(eytan-starkware): Accept a collection of vars to create a box of. A single call to
245+
// struct_boxed_deconstruct can be created for multiple vars. When creating multivars
246+
// we need to put creation at a dominating point.
247+
248+
let boxed_struct_ty = variables[boxed_struct_var].ty;
249+
trace!("Creating struct_boxed_deconstruct call for type {:?}", boxed_struct_ty);
250+
251+
// Extract the struct type from Box<Struct>
252+
// The boxed type should be Box<T>, we need to get T
253+
let TypeLongId::Concrete(concrete_box) = boxed_struct_ty.long(db) else {
254+
unreachable!("Unbox should always be called on a box type (which is concrete).");
255+
};
256+
257+
let generic_args = concrete_box.generic_args(db);
258+
let GenericArgumentId::Type(inner_ty) = generic_args.first()? else {
259+
unreachable!("Box unbox call should always have a generic arg");
260+
};
261+
262+
if db.copyable(*inner_ty).is_err() {
263+
return None;
264+
}
265+
let (n_snapshots, struct_ty) = peel_snapshots(db, *inner_ty);
266+
267+
// TODO(eytan-starkware): Support snapshots of structs in reboxing optimization.
268+
// Currently we give up if the struct is wrapped in snapshots.
269+
if n_snapshots > 0 {
270+
trace!("Skipping reboxing for snapshotted struct (n_snapshots={})", n_snapshots);
271+
return None;
272+
}
273+
274+
trace!("Extracted struct or tuple type: {:?}", struct_ty);
275+
276+
// Get the type info to determine number of members
277+
let (num_members, member_types): (usize, Vec<TypeId<'_>>) = match struct_ty {
278+
TypeLongId::Concrete(ConcreteTypeId::Struct(struct_id)) => {
279+
let members = db.concrete_struct_members(struct_id).ok()?;
280+
let num = members.len();
281+
let types = members.iter().map(|(_, member)| member.ty).collect();
282+
(num, types)
283+
}
284+
TypeLongId::Tuple(inner_types) => {
285+
let num = inner_types.len();
286+
(num, inner_types)
287+
}
288+
_ => {
289+
trace!("Unsupported type for reboxing: {:?}", struct_ty);
290+
return None;
291+
}
292+
};
293+
294+
if member_types.iter().any(|ty| db.droppable(*ty).is_err()) {
295+
trace!("Type contains droppable members. Currently unsupported, skipping.");
296+
return None;
297+
}
298+
299+
trace!("Type has {} members, accessing member {}", num_members, member_index);
300+
301+
if member_index >= num_members {
302+
unreachable!("Member index out of bounds");
303+
}
304+
305+
// Create output variables for all members (all will be Box<MemberType>)
306+
// We'll create new variables except for the one we're interested in
307+
let mut outputs = Vec::new();
308+
for (idx, member_ty) in member_types.into_iter().enumerate() {
309+
if idx == member_index {
310+
// Use the existing output variable
311+
outputs.push(output_var);
312+
} else {
313+
// Create a new variable for this member
314+
// The type should be Box<member_ty>
315+
let box_ty = cairo_lang_semantic::corelib::core_box_ty(db, member_ty);
316+
let out_location = variables[output_var].location;
317+
let var = variables.alloc(Variable::with_default_context(db, box_ty, out_location));
318+
outputs.push(var);
319+
}
320+
}
321+
322+
// Create the call statement
323+
let old_input = old_stmt.inputs()[0];
324+
let stmt = Statement::StructDestructure(StatementStructDestructure {
325+
input: VarUsage { var_id: boxed_struct_var, location: old_input.location },
326+
outputs,
327+
});
328+
329+
Some(stmt)
330+
}

0 commit comments

Comments
 (0)