Skip to content

Commit e4c20b9

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

File tree

10 files changed

+1061
-0
lines changed

10 files changed

+1061
-0
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cairo-lang-lowering/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ salsa.workspace = true
2828
serde = { workspace = true, default-features = true }
2929
starknet-types-core.workspace = true
3030
thiserror.workspace = true
31+
tracing = { workspace = true, features = ["log"] }
32+
tracing-subscriber = { workspace = true, features = ["env-filter"] }
33+
3134

3235
[dev-dependencies]
3336
cairo-lang-plugins = { path = "../cairo-lang-plugins" }

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

0 commit comments

Comments
 (0)