Skip to content

Commit d9cde44

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

File tree

12 files changed

+1090
-2
lines changed

12 files changed

+1090
-2
lines changed

Cargo.lock

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

crates/cairo-lang-filesystem/src/flag.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,19 @@ 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 as it makes panic unprovable.
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+
pub fn flag_future_sierra(db: &dyn salsa::Database) -> bool {
36+
let flag = FlagId::new(db, FlagLongId("future_sierra".into()));
37+
if let Some(flag) = db.get_flag(flag) { *flag == Flag::FutureSierra(true) } else { false }
38+
}

crates/cairo-lang-lowering/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ 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+
3133

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

0 commit comments

Comments
 (0)