Skip to content

Commit 04c6b94

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

File tree

9 files changed

+1262
-2
lines changed

9 files changed

+1262
-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: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
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, 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+
for (stmt_idx, stmt) in block.statements.iter().enumerate() {
80+
match stmt {
81+
Statement::Call(call_stmt) => {
82+
if let Some((extern_id, _)) = call_stmt.function.get_extern(db) {
83+
if extern_id == unbox_id {
84+
let res = ReboxingValue::Unboxed(call_stmt.inputs[0].var_id);
85+
current_state.insert(call_stmt.outputs[0], res);
86+
} else if extern_id == into_box_id {
87+
let source = current_state
88+
.get(&call_stmt.inputs[0].var_id)
89+
.unwrap_or(&ReboxingValue::Revoked);
90+
if matches!(source, ReboxingValue::Revoked) {
91+
continue;
92+
}
93+
candidates.insert(ReboxCandidate {
94+
source: source.clone(),
95+
reboxed_var: call_stmt.outputs[0],
96+
into_box_location: (block_id, stmt_idx),
97+
});
98+
}
99+
}
100+
}
101+
Statement::StructDestructure(destructure_stmt) => {
102+
let input_state = current_state
103+
.get(&destructure_stmt.input.var_id)
104+
.cloned()
105+
.unwrap_or(ReboxingValue::Revoked);
106+
match input_state {
107+
ReboxingValue::Revoked => {}
108+
ReboxingValue::MemberOfUnboxed { .. } | ReboxingValue::Unboxed(_) => {
109+
for (member_idx, output_var) in
110+
destructure_stmt.outputs.iter().enumerate()
111+
{
112+
let res = ReboxingValue::MemberOfUnboxed {
113+
source: Rc::new(input_state.clone()),
114+
member: member_idx,
115+
};
116+
117+
current_state.insert(*output_var, res);
118+
}
119+
}
120+
}
121+
}
122+
_ => {}
123+
}
124+
}
125+
126+
// Process block end to handle variable remapping
127+
if let BlockEnd::Goto(_, remapping) = &block.end {
128+
for (dst, src_usage) in remapping.iter() {
129+
let src_state =
130+
current_state.get(&src_usage.var_id).cloned().unwrap_or(ReboxingValue::Revoked);
131+
update_reboxing_variable_join(&mut current_state, *dst, src_state);
132+
}
133+
}
134+
}
135+
136+
trace!("Found {} reboxing candidate(s).", candidates.len());
137+
candidates
138+
}
139+
140+
/// Update the reboxing state for a variable join. If the variable is already in the state with a
141+
/// different value, it is revoked.
142+
fn update_reboxing_variable_join(
143+
current_state: &mut OrderedHashMap<id_arena::Id<crate::VariableMarker>, ReboxingValue>,
144+
var: VariableId,
145+
res: ReboxingValue,
146+
) {
147+
match current_state.entry(var) {
148+
Entry::Vacant(entry) => {
149+
entry.insert(res);
150+
}
151+
Entry::Occupied(mut entry) => {
152+
if entry.get() != &res {
153+
entry.insert(ReboxingValue::Revoked);
154+
}
155+
}
156+
}
157+
}
158+
159+
/// Applies reboxing optimizations to the lowered function using the provided candidates.
160+
pub fn apply_reboxing_candidates<'db>(
161+
db: &'db dyn Database,
162+
lowered: &mut Lowered<'db>,
163+
candidates: &OrderedHashSet<ReboxCandidate>,
164+
) {
165+
if candidates.is_empty() {
166+
trace!("No reboxing candidates to apply.");
167+
return;
168+
}
169+
170+
trace!("Applying {} reboxing optimization(s).", candidates.len());
171+
172+
for candidate in candidates {
173+
apply_reboxing_candidate(db, lowered, candidate);
174+
}
175+
}
176+
177+
/// Applies the reboxing optimization to the lowered function.
178+
///
179+
/// This optimization detects patterns where we:
180+
/// 1. Unbox a struct
181+
/// 2. (Optional) Destructure it
182+
/// 3. Box one of the members back
183+
///
184+
/// And replaces it with a direct struct_boxed_deconstruct libfunc call.
185+
pub fn apply_reboxing<'db>(db: &'db dyn Database, lowered: &mut Lowered<'db>) {
186+
if flag_future_sierra(db) {
187+
let candidates = find_reboxing_candidates(db, lowered);
188+
apply_reboxing_candidates(db, lowered, &candidates);
189+
}
190+
}
191+
192+
/// Applies a single reboxing optimization for the given candidate.
193+
fn apply_reboxing_candidate<'db>(
194+
db: &'db dyn Database,
195+
lowered: &mut Lowered<'db>,
196+
candidate: &ReboxCandidate,
197+
) {
198+
trace!(
199+
"Applying optimization: candidate={:?}, reboxed={}",
200+
candidate.source,
201+
candidate.reboxed_var.index()
202+
);
203+
204+
// TODO(eytan-starkware): Handle snapshot of box (e.g., @Box<T>).
205+
// Only support MemberOfUnboxed where source is Unboxed for now.
206+
let ReboxingValue::MemberOfUnboxed { source, member } = &candidate.source else {
207+
// If source is not member of unboxed, we are reboxing original value which is not supported
208+
// yet.
209+
return;
210+
};
211+
let ReboxingValue::Unboxed(source_var) = **source else {
212+
// When source of the value is not `Unboxes`, it is a nested MemberOfUnboxed, which is not
213+
// supported yet.
214+
return;
215+
};
216+
// Create the struct_boxed_deconstruct call
217+
let (into_box_block, into_box_stmt_idx) = candidate.into_box_location;
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[into_box_block].statements[into_box_stmt_idx],
225+
) {
226+
lowered.blocks[into_box_block].statements[into_box_stmt_idx] = new_stmt;
227+
trace!("Successfully applied reboxing optimization.");
228+
}
229+
}
230+
231+
/// Creates a struct_boxed_deconstruct call statement.
232+
/// Returns None if the call cannot be created.
233+
fn create_struct_boxed_deconstruct_call<'db>(
234+
db: &'db dyn Database,
235+
variables: &mut VariableArena<'db>,
236+
boxed_struct_var: VariableId,
237+
member_index: usize,
238+
output_var: VariableId,
239+
old_stmt: &Statement<'db>,
240+
) -> Option<Statement<'db>> {
241+
let boxed_struct_ty = variables[boxed_struct_var].ty;
242+
trace!("Creating struct_boxed_deconstruct call for type {:?}", boxed_struct_ty);
243+
244+
// Extract the struct type from Box<Struct>
245+
// The boxed type should be Box<T>, we need to get T
246+
let TypeLongId::Concrete(concrete_box) = boxed_struct_ty.long(db) else {
247+
unreachable!("Unbox should always be called on a box type (which is concrete).");
248+
};
249+
250+
let generic_args = concrete_box.generic_args(db);
251+
let GenericArgumentId::Type(inner_ty) = generic_args.first()? else {
252+
unreachable!("Box unbox call should always have a generic arg");
253+
};
254+
255+
if db.copyable(*inner_ty).is_err() {
256+
return None;
257+
}
258+
let (n_snapshots, struct_ty) = peel_snapshots(db, *inner_ty);
259+
260+
// TODO(eytan-starkware): Support snapshots of structs in reboxing optimization.
261+
// Currently we give up if the struct is wrapped in snapshots.
262+
if n_snapshots > 0 {
263+
trace!("Skipping reboxing for snapshotted struct (n_snapshots={})", n_snapshots);
264+
return None;
265+
}
266+
267+
// Extract member types from struct or tuple
268+
let member_types = match struct_ty {
269+
TypeLongId::Concrete(ConcreteTypeId::Struct(struct_id)) => db
270+
.concrete_struct_members(struct_id)
271+
.ok()?
272+
.iter()
273+
.map(|(_, member)| member.ty)
274+
.collect::<Vec<_>>(),
275+
TypeLongId::Tuple(inner_types) => inner_types,
276+
_ => {
277+
trace!("Unsupported type for reboxing: {:?}", struct_ty);
278+
return None;
279+
}
280+
};
281+
282+
if member_types.iter().any(|ty| db.droppable(*ty).is_err()) {
283+
trace!("Type contains droppable members. Currently unsupported, skipping.");
284+
return None;
285+
}
286+
trace!("Type has {} members, accessing member {}", member_types.len(), member_index);
287+
288+
if member_index >= member_types.len() {
289+
unreachable!("Member index out of bounds");
290+
}
291+
292+
// Create output variables for all members (all will be Box<MemberType>)
293+
// We'll create new variables except for the one we're interested in
294+
let mut outputs = Vec::new();
295+
for (idx, member_ty) in member_types.into_iter().enumerate() {
296+
if idx == member_index {
297+
outputs.push(output_var);
298+
} else {
299+
let box_ty = cairo_lang_semantic::corelib::core_box_ty(db, member_ty);
300+
let out_location = variables[output_var].location;
301+
let var = variables.alloc(Variable::with_default_context(db, box_ty, out_location));
302+
outputs.push(var);
303+
}
304+
}
305+
306+
Some(Statement::StructDestructure(StatementStructDestructure {
307+
input: VarUsage { var_id: boxed_struct_var, location: old_stmt.inputs()[0].location },
308+
outputs,
309+
}))
310+
}

0 commit comments

Comments
 (0)