Skip to content

Commit e6c116b

Browse files
(optimization): automatically detecting reboxing of struct member and applying struct_box_deconstruct libfunc
1 parent 8519204 commit e6c116b

File tree

9 files changed

+1237
-2
lines changed

9 files changed

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

0 commit comments

Comments
 (0)