Skip to content

Commit beb9849

Browse files
(optimization): automatically detecting reboxing of struct member and applying struct_box_deconstruct libfunc
1 parent 0590aee commit beb9849

File tree

9 files changed

+1402
-2
lines changed

9 files changed

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

0 commit comments

Comments
 (0)