Skip to content

Commit fd79ad9

Browse files
(optimization): automatically detecting reboxing of struct member and applying struct_box_deconstruct libfunc
1 parent 6bc9eba commit fd79ad9

File tree

10 files changed

+1223
-2
lines changed

10 files changed

+1223
-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/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub mod add_withdraw_gas;
55
pub mod borrow_check;
66
pub mod cache;
77
pub mod concretize;
8+
89
pub mod db;
910
pub mod destructs;
1011
pub mod diagnostic;

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: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
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_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::OrderedHashMap;
13+
use cairo_lang_utils::ordered_hash_set::OrderedHashSet;
14+
use salsa::Database;
15+
16+
use crate::{
17+
BlockEnd, BlockId, Lowered, Statement, StatementStructDestructure, VarUsage, Variable,
18+
VariableArena, VariableId,
19+
};
20+
21+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
22+
pub enum ReboxingValue {
23+
Nothing,
24+
Unboxed(VariableId),
25+
MemberOfUnboxed { source: Rc<ReboxingValue>, member: usize },
26+
}
27+
28+
impl Display for ReboxingValue {
29+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30+
match self {
31+
ReboxingValue::Nothing => write!(f, "Nothing"),
32+
ReboxingValue::Unboxed(id) => write!(f, "Unboxed({})", id.index()),
33+
ReboxingValue::MemberOfUnboxed { source, member } => {
34+
write!(f, "MemberOfUnboxed({}, {})", source, member)
35+
}
36+
}
37+
}
38+
}
39+
40+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
41+
pub struct ReboxCandidate {
42+
/// The reboxing data
43+
pub source: ReboxingValue,
44+
/// The reboxed variable (output of into_box)
45+
pub reboxed_var: VariableId,
46+
/// Location where into_box call occurs (block_id, stmt_idx)
47+
pub into_box_location: (BlockId, usize),
48+
}
49+
50+
/// Finds reboxing candidates in the lowered function.
51+
///
52+
/// This analysis detects patterns where we:
53+
/// 1. Unbox a struct
54+
/// 2. (Optional) Destructure it
55+
/// 3. Box one of the members back
56+
///
57+
/// Returns candidates that can be optimized with struct_boxed_deconstruct libfunc calls.
58+
pub fn find_reboxing_candidates<'db>(
59+
db: &'db dyn Database,
60+
lowered: &Lowered<'db>,
61+
) -> OrderedHashSet<ReboxCandidate> {
62+
if lowered.blocks.is_empty() {
63+
return OrderedHashSet::default();
64+
}
65+
66+
trace!("Running reboxing analysis...");
67+
68+
let core = ModuleHelper::core(db);
69+
let box_module = core.submodule("box");
70+
let unbox_id = box_module.extern_function_id("unbox");
71+
let into_box_id = box_module.extern_function_id("into_box");
72+
73+
// TODO(eytan-starkware): Support "snapshot" equality tracking in the reboxing analysis.
74+
// Currently we track unboxed values and their members, but we don't properly handle
75+
// the case where snapshots are taken and we need to track that a snapshot of a member
76+
// is equivalent to a member of a snapshot.
77+
78+
let mut current_state: OrderedHashMap<VariableId, ReboxingValue> = OrderedHashMap::default();
79+
let mut candidates: OrderedHashSet<ReboxCandidate> = OrderedHashSet::default();
80+
81+
// Worklist algorithm is just iterating the blocks as we run after block reordering which gives
82+
// us a topological sort (TODO: add to doc)
83+
for (block_id, block) in lowered.blocks.iter() {
84+
// Process statements
85+
for (stmt_idx, stmt) in block.statements.iter().enumerate() {
86+
match stmt {
87+
Statement::Call(call_stmt) => {
88+
if let Some((extern_id, _)) = call_stmt.function.get_extern(db) {
89+
if extern_id == unbox_id {
90+
let res = ReboxingValue::Unboxed(call_stmt.inputs[0].var_id);
91+
if let Some(old) =
92+
current_state.insert(call_stmt.outputs[0], res.clone())
93+
&& old != res
94+
{
95+
current_state.insert(call_stmt.outputs[0], ReboxingValue::Nothing);
96+
}
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::Nothing);
101+
if matches!(source, ReboxingValue::Nothing) {
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::Nothing);
117+
match input_state {
118+
ReboxingValue::Nothing => {}
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+
if let Some(old) = current_state.insert(*output_var, res.clone())
128+
&& res != old
129+
{
130+
current_state.insert(*output_var, ReboxingValue::Nothing);
131+
}
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::Nothing);
145+
if let Some(existing) = current_state.insert(*dst, src_state.clone())
146+
&& existing != src_state
147+
{
148+
current_state.insert(*dst, ReboxingValue::Nothing);
149+
}
150+
}
151+
}
152+
}
153+
154+
trace!("Found {} reboxing candidate(s).", candidates.len());
155+
candidates
156+
}
157+
158+
/// Applies reboxing optimizations to the lowered function using the provided candidates.
159+
pub fn apply_reboxing_candidates<'db>(
160+
db: &'db dyn Database,
161+
lowered: &mut Lowered<'db>,
162+
candidates: &OrderedHashSet<ReboxCandidate>,
163+
) {
164+
if candidates.is_empty() {
165+
trace!("No reboxing candidates to apply.");
166+
return;
167+
}
168+
169+
trace!("Applying {} reboxing optimization(s).", candidates.len());
170+
171+
for candidate in candidates {
172+
apply_reboxing_candidate(db, lowered, candidate);
173+
}
174+
}
175+
176+
/// Applies the reboxing optimization to the lowered function.
177+
///
178+
/// This optimization detects patterns where we:
179+
/// 1. Unbox a struct
180+
/// 2. (Optional) Destructure it
181+
/// 3. Box one of the members back
182+
///
183+
/// And replaces it with a direct struct_boxed_deconstruct libfunc call.
184+
pub fn apply_reboxing<'db>(db: &'db dyn Database, lowered: &mut Lowered<'db>) {
185+
let candidates = find_reboxing_candidates(db, lowered);
186+
apply_reboxing_candidates(db, lowered, &candidates);
187+
}
188+
189+
/// Applies a single reboxing optimization for the given candidate.
190+
fn apply_reboxing_candidate<'db>(
191+
db: &'db dyn Database,
192+
lowered: &mut Lowered<'db>,
193+
candidate: &ReboxCandidate,
194+
) {
195+
trace!(
196+
"Applying optimization: candidate={}, reboxed={}",
197+
candidate.source,
198+
candidate.reboxed_var.index()
199+
);
200+
201+
// Only support MemberOfUnboxed where source is Unboxed for now.
202+
if let ReboxingValue::MemberOfUnboxed { source, member } = &candidate.source {
203+
if let ReboxingValue::Unboxed(source_var) = **source {
204+
// Create the struct_boxed_deconstruct call
205+
if let Some(new_stmt) = create_struct_boxed_deconstruct_call(
206+
db,
207+
&mut lowered.variables,
208+
source_var,
209+
*member,
210+
candidate.reboxed_var,
211+
&lowered.blocks[candidate.into_box_location.0].statements
212+
[candidate.into_box_location.1],
213+
) {
214+
// swap to the new call
215+
let (into_box_block, into_box_stmt_idx) = candidate.into_box_location;
216+
lowered.blocks[into_box_block].statements[into_box_stmt_idx] = new_stmt;
217+
218+
trace!("Successfully applied reboxing optimization.");
219+
}
220+
} else {
221+
// Nested MemberOfUnboxed not supported yet.
222+
}
223+
} else {
224+
// Unboxed reboxing not supported yet.
225+
}
226+
}
227+
228+
/// Creates a struct_boxed_deconstruct call statement.
229+
/// Returns None if the call cannot be created.
230+
fn create_struct_boxed_deconstruct_call<'db>(
231+
db: &'db dyn Database,
232+
variables: &mut VariableArena<'db>,
233+
boxed_struct_var: VariableId,
234+
member_index: usize,
235+
output_var: VariableId,
236+
old_stmt: &Statement<'db>,
237+
) -> Option<Statement<'db>> {
238+
// TODO(eytan-starkware): Accept a collection of vars to create a box of. A single call to
239+
// struct_boxed_deconstruct can be created for multiple vars. When creating multivars
240+
// we need to put creation at a dominating point.
241+
242+
let boxed_struct_ty = variables[boxed_struct_var].ty;
243+
trace!("Creating struct_boxed_deconstruct call for type {:?}", boxed_struct_ty);
244+
245+
// Extract the struct type from Box<Struct>
246+
// The boxed type should be Box<T>, we need to get T
247+
let TypeLongId::Concrete(concrete_box) = boxed_struct_ty.long(db) else {
248+
unreachable!("Unbox should always be called on a box type (which is concrete).");
249+
};
250+
251+
let generic_args = concrete_box.generic_args(db);
252+
// TODO: Why isnt this a box??????
253+
let GenericArgumentId::Type(inner_ty) = generic_args.first()? else {
254+
unreachable!("Box unbox call should always have a generic arg");
255+
};
256+
257+
if db.copyable(TypeId::new(db, inner_ty.long(db))).is_err() {
258+
return None;
259+
}
260+
let (n_snapshots, struct_ty) = peel_snapshots(db, *inner_ty);
261+
262+
// TODO(eytan-starkware): Support snapshots of structs in reboxing optimization.
263+
// Currently we give up if the struct is wrapped in snapshots.
264+
if n_snapshots > 0 {
265+
trace!("Skipping reboxing for snapshotted struct (n_snapshots={})", n_snapshots);
266+
return None;
267+
}
268+
269+
trace!("Extracted struct or tuple type: {:?}", struct_ty);
270+
271+
// Get the type info to determine number of members
272+
let (num_members, member_types): (usize, Vec<TypeId<'_>>) = match struct_ty {
273+
TypeLongId::Concrete(ConcreteTypeId::Struct(struct_id)) => {
274+
let members = db.concrete_struct_members(struct_id).ok()?;
275+
let num = members.len();
276+
let types = members.iter().map(|(_, member)| member.ty).collect();
277+
(num, types)
278+
}
279+
TypeLongId::Tuple(inner_types) => {
280+
let num = inner_types.len();
281+
(num, inner_types)
282+
}
283+
_ => {
284+
trace!("Unsupported type for reboxing: {:?}", struct_ty);
285+
return None;
286+
}
287+
};
288+
289+
trace!("Type has {} members, accessing member {}", num_members, member_index);
290+
291+
if member_index >= num_members {
292+
unreachable!("Member index out of bounds");
293+
}
294+
295+
// Create output variables for all members (all will be Box<MemberType>)
296+
// We'll create new variables except for the one we're interested in
297+
let mut outputs = Vec::new();
298+
for (idx, member_ty) in member_types.into_iter().enumerate() {
299+
if idx == member_index {
300+
// Use the existing output variable
301+
outputs.push(output_var);
302+
} else {
303+
// Create a new variable for this member
304+
// The type should be Box<member_ty>
305+
let box_ty = cairo_lang_semantic::corelib::core_box_ty(db, member_ty);
306+
let out_location = variables[output_var].location;
307+
let var = variables.alloc(Variable::with_default_context(db, box_ty, out_location));
308+
outputs.push(var);
309+
}
310+
}
311+
312+
// Create the call statement
313+
let old_input = old_stmt.inputs()[0];
314+
let stmt = Statement::StructDestructure(StatementStructDestructure {
315+
input: VarUsage { var_id: boxed_struct_var, location: old_input.location },
316+
outputs,
317+
});
318+
319+
Some(stmt)
320+
}

0 commit comments

Comments
 (0)