Skip to content

Commit 73ca092

Browse files
(optimization): automatically detecting reboxing of struct member and applying struct_box_deconstruct libfunc
1 parent 210361d commit 73ca092

File tree

9 files changed

+1296
-2
lines changed

9 files changed

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

0 commit comments

Comments
 (0)