diff --git a/crates/cairo-lang-filesystem/src/flag.rs b/crates/cairo-lang-filesystem/src/flag.rs index 8b917658587..e4ec3c428fa 100644 --- a/crates/cairo-lang-filesystem/src/flag.rs +++ b/crates/cairo-lang-filesystem/src/flag.rs @@ -20,6 +20,10 @@ pub enum Flag { /// /// Default is false as it makes panic unprovable. UnsafePanic(bool), + /// Whether to use future_sierra in the generated code. + /// + /// Default is false. + FutureSierra(bool), } /// Returns the value of the `unsafe_panic` flag, or `false` if the flag is not set. @@ -27,3 +31,9 @@ pub fn flag_unsafe_panic(db: &dyn salsa::Database) -> bool { let flag = FlagId::new(db, FlagLongId("unsafe_panic".into())); if let Some(flag) = db.get_flag(flag) { *flag == Flag::UnsafePanic(true) } else { false } } + +/// Returns the value of the `future_sierra` flag, or `false` if the flag is not set. +pub fn flag_future_sierra(db: &dyn salsa::Database) -> bool { + let flag = FlagId::new(db, FlagLongId("future_sierra".into())); + if let Some(flag) = db.get_flag(flag) { *flag == Flag::FutureSierra(true) } else { false } +} diff --git a/crates/cairo-lang-lowering/src/optimizations/mod.rs b/crates/cairo-lang-lowering/src/optimizations/mod.rs index df5a80a67ef..3efb9a641ec 100644 --- a/crates/cairo-lang-lowering/src/optimizations/mod.rs +++ b/crates/cairo-lang-lowering/src/optimizations/mod.rs @@ -27,6 +27,7 @@ pub mod dedup_blocks; pub mod early_unsafe_panic; pub mod gas_redeposit; pub mod match_optimizer; +pub mod reboxing; pub mod remappings; pub mod reorder_statements; pub mod return_optimization; diff --git a/crates/cairo-lang-lowering/src/optimizations/reboxing.rs b/crates/cairo-lang-lowering/src/optimizations/reboxing.rs new file mode 100644 index 00000000000..1441c6689cb --- /dev/null +++ b/crates/cairo-lang-lowering/src/optimizations/reboxing.rs @@ -0,0 +1,310 @@ +#[cfg(test)] +#[path = "reboxing_test.rs"] +mod reboxing_test; + +use std::rc::Rc; + +use cairo_lang_filesystem::flag::flag_future_sierra; +use cairo_lang_semantic::helper::ModuleHelper; +use cairo_lang_semantic::items::structure::StructSemantic; +use cairo_lang_semantic::types::{TypesSemantic, peel_snapshots}; +use cairo_lang_semantic::{ConcreteTypeId, GenericArgumentId, TypeLongId}; +use cairo_lang_utils::ordered_hash_map::{Entry, OrderedHashMap}; +use cairo_lang_utils::ordered_hash_set::OrderedHashSet; +use salsa::Database; + +use crate::borrow_check::analysis::StatementLocation; +use crate::{ + BlockEnd, Lowered, Statement, StatementStructDestructure, VarUsage, Variable, VariableArena, + VariableId, +}; + +/// The possible values for the reboxing analysis. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ReboxingValue { + /// No reboxing can be done. Relevant after a meet of two paths. + Revoked, + /// The variable is unboxed from a different variable. + Unboxed(VariableId), + /// The variable is a member of an unboxed variable. + MemberOfUnboxed { source: Rc, member: usize }, +} + +/// Represents a candidate for reboxing optimization. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct ReboxCandidate { + /// The reboxing data + pub source: ReboxingValue, + /// The reboxed variable (output of into_box) + pub reboxed_var: VariableId, + /// Location where into_box call occurs (block_id, stmt_idx) + pub into_box_location: StatementLocation, +} + +/// Finds reboxing candidates in the lowered function. Assumes a topological sort of blocks. +/// +/// This analysis detects patterns where we: +/// 1. Unbox a struct +/// 2. (Optional) Destructure it +/// 3. Box one of the members back +/// +/// Returns candidates that can be optimized with struct_boxed_deconstruct libfunc calls. +pub fn find_reboxing_candidates<'db>( + db: &'db dyn Database, + lowered: &Lowered<'db>, +) -> OrderedHashSet { + if lowered.blocks.is_empty() { + return OrderedHashSet::default(); + } + + trace!("Running reboxing analysis..."); + + let core = ModuleHelper::core(db); + let box_module = core.submodule("box"); + let unbox_id = box_module.extern_function_id("unbox"); + let into_box_id = box_module.extern_function_id("into_box"); + + // TODO(eytan-starkware): When applied, reboxing analysis should replace the existing + // deconstruct with a boxed-deconstruct, and add unbox statements on members as needed. + + // TODO(eytan-starkware): Support "snapshot" equality tracking in the reboxing analysis. + // Currently we track unboxed values and their members, but we don't properly handle + // the case where snapshots are taken and we need to track that a snapshot of a member + // is equivalent to a member of a snapshot. + + let mut current_state: OrderedHashMap = Default::default(); + let mut candidates: OrderedHashSet = Default::default(); + + for (block_id, block) in lowered.blocks.iter() { + for (stmt_idx, stmt) in block.statements.iter().enumerate() { + match stmt { + Statement::Call(call_stmt) => { + if let Some((extern_id, _)) = call_stmt.function.get_extern(db) { + if extern_id == unbox_id { + let res = ReboxingValue::Unboxed(call_stmt.inputs[0].var_id); + current_state.insert(call_stmt.outputs[0], res); + } else if extern_id == into_box_id { + let source = current_state + .get(&call_stmt.inputs[0].var_id) + .unwrap_or(&ReboxingValue::Revoked); + if matches!(source, ReboxingValue::Revoked) { + continue; + } + candidates.insert(ReboxCandidate { + source: source.clone(), + reboxed_var: call_stmt.outputs[0], + into_box_location: (block_id, stmt_idx), + }); + } + } + } + Statement::StructDestructure(destructure_stmt) => { + let input_state = current_state + .get(&destructure_stmt.input.var_id) + .cloned() + .unwrap_or(ReboxingValue::Revoked); + match input_state { + ReboxingValue::Revoked => {} + ReboxingValue::MemberOfUnboxed { .. } | ReboxingValue::Unboxed(_) => { + for (member_idx, output_var) in + destructure_stmt.outputs.iter().enumerate() + { + let res = ReboxingValue::MemberOfUnboxed { + source: Rc::new(input_state.clone()), + member: member_idx, + }; + + current_state.insert(*output_var, res); + } + } + } + } + _ => {} + } + } + + // Process block end to handle variable remapping + if let BlockEnd::Goto(_, remapping) = &block.end { + for (dst, src_usage) in remapping.iter() { + let src_state = + current_state.get(&src_usage.var_id).cloned().unwrap_or(ReboxingValue::Revoked); + update_reboxing_variable_join(&mut current_state, *dst, src_state); + } + } + } + + trace!("Found {} reboxing candidate(s).", candidates.len()); + candidates +} + +/// Update the reboxing state for a variable join. If the variable is already in the state with a +/// different value, it is revoked. +fn update_reboxing_variable_join( + current_state: &mut OrderedHashMap, ReboxingValue>, + var: VariableId, + res: ReboxingValue, +) { + match current_state.entry(var) { + Entry::Vacant(entry) => { + entry.insert(res); + } + Entry::Occupied(mut entry) => { + if entry.get() != &res { + entry.insert(ReboxingValue::Revoked); + } + } + } +} + +/// Applies reboxing optimizations to the lowered function using the provided candidates. +pub fn apply_reboxing_candidates<'db>( + db: &'db dyn Database, + lowered: &mut Lowered<'db>, + candidates: &OrderedHashSet, +) { + if candidates.is_empty() { + trace!("No reboxing candidates to apply."); + return; + } + + trace!("Applying {} reboxing optimization(s).", candidates.len()); + + for candidate in candidates { + apply_reboxing_candidate(db, lowered, candidate); + } +} + +/// Applies the reboxing optimization to the lowered function. +/// +/// This optimization detects patterns where we: +/// 1. Unbox a struct +/// 2. (Optional) Destructure it +/// 3. Box one of the members back +/// +/// And replaces it with a direct struct_boxed_deconstruct libfunc call. +pub fn apply_reboxing<'db>(db: &'db dyn Database, lowered: &mut Lowered<'db>) { + if flag_future_sierra(db) { + let candidates = find_reboxing_candidates(db, lowered); + apply_reboxing_candidates(db, lowered, &candidates); + } +} + +/// Applies a single reboxing optimization for the given candidate. +fn apply_reboxing_candidate<'db>( + db: &'db dyn Database, + lowered: &mut Lowered<'db>, + candidate: &ReboxCandidate, +) { + trace!( + "Applying optimization: candidate={:?}, reboxed={}", + candidate.source, + candidate.reboxed_var.index() + ); + + // TODO(eytan-starkware): Handle snapshot of box (e.g., @Box). + // Only support MemberOfUnboxed where source is Unboxed for now. + let ReboxingValue::MemberOfUnboxed { source, member } = &candidate.source else { + // If source is not member of unboxed, we are reboxing original value which is not supported + // yet. + return; + }; + let ReboxingValue::Unboxed(source_var) = **source else { + // When source of the value is not `Unboxes`, it is a nested MemberOfUnboxed, which is not + // supported yet. + return; + }; + // Create the struct_boxed_deconstruct call + let (into_box_block, into_box_stmt_idx) = candidate.into_box_location; + if let Some(new_stmt) = create_struct_boxed_deconstruct_call( + db, + &mut lowered.variables, + source_var, + *member, + candidate.reboxed_var, + &lowered.blocks[into_box_block].statements[into_box_stmt_idx], + ) { + lowered.blocks[into_box_block].statements[into_box_stmt_idx] = new_stmt; + trace!("Successfully applied reboxing optimization."); + } +} + +/// Creates a struct_boxed_deconstruct call statement. +/// Returns None if the call cannot be created. +fn create_struct_boxed_deconstruct_call<'db>( + db: &'db dyn Database, + variables: &mut VariableArena<'db>, + boxed_struct_var: VariableId, + member_index: usize, + output_var: VariableId, + old_stmt: &Statement<'db>, +) -> Option> { + let boxed_struct_ty = variables[boxed_struct_var].ty; + trace!("Creating struct_boxed_deconstruct call for type {:?}", boxed_struct_ty); + + // Extract the struct type from Box + // The boxed type should be Box, we need to get T + let TypeLongId::Concrete(concrete_box) = boxed_struct_ty.long(db) else { + unreachable!("Unbox should always be called on a box type (which is concrete)."); + }; + + let generic_args = concrete_box.generic_args(db); + let GenericArgumentId::Type(inner_ty) = generic_args.first()? else { + unreachable!("Box unbox call should always have a generic arg"); + }; + + if db.copyable(*inner_ty).is_err() { + return None; + } + let (n_snapshots, struct_ty) = peel_snapshots(db, *inner_ty); + + // TODO(eytan-starkware): Support snapshots of structs in reboxing optimization. + // Currently we give up if the struct is wrapped in snapshots. + if n_snapshots > 0 { + trace!("Skipping reboxing for snapshotted struct (n_snapshots={})", n_snapshots); + return None; + } + + // Extract member types from struct or tuple + let member_types = match struct_ty { + TypeLongId::Concrete(ConcreteTypeId::Struct(struct_id)) => db + .concrete_struct_members(struct_id) + .ok()? + .iter() + .map(|(_, member)| member.ty) + .collect::>(), + TypeLongId::Tuple(inner_types) => inner_types, + _ => { + trace!("Unsupported type for reboxing: {:?}", struct_ty); + return None; + } + }; + + if member_types.iter().any(|ty| db.droppable(*ty).is_err()) { + trace!("Type contains droppable members. Currently unsupported, skipping."); + return None; + } + trace!("Type has {} members, accessing member {}", member_types.len(), member_index); + + if member_index >= member_types.len() { + unreachable!("Member index out of bounds"); + } + + // Create output variables for all members (all will be Box) + // We'll create new variables except for the one we're interested in + let mut outputs = Vec::new(); + for (idx, member_ty) in member_types.into_iter().enumerate() { + if idx == member_index { + outputs.push(output_var); + } else { + let box_ty = cairo_lang_semantic::corelib::core_box_ty(db, member_ty); + let out_location = variables[output_var].location; + let var = variables.alloc(Variable::with_default_context(db, box_ty, out_location)); + outputs.push(var); + } + } + + Some(Statement::StructDestructure(StatementStructDestructure { + input: VarUsage { var_id: boxed_struct_var, location: old_stmt.inputs()[0].location }, + outputs, + })) +} diff --git a/crates/cairo-lang-lowering/src/optimizations/reboxing_test.rs b/crates/cairo-lang-lowering/src/optimizations/reboxing_test.rs new file mode 100644 index 00000000000..89bae54b8ee --- /dev/null +++ b/crates/cairo-lang-lowering/src/optimizations/reboxing_test.rs @@ -0,0 +1,76 @@ +use cairo_lang_debug::DebugWithDb; +use cairo_lang_semantic::test_utils::setup_test_function; +use cairo_lang_test_utils::parse_test_file::TestRunnerResult; +use cairo_lang_utils::ordered_hash_map::OrderedHashMap; + +use crate::LoweringStage; +use crate::db::LoweringGroup; +use crate::fmt::LoweredFormatter; +use crate::ids::ConcreteFunctionWithBodyId; +use crate::optimizations::reboxing::{apply_reboxing_candidates, find_reboxing_candidates}; +use crate::optimizations::strategy::OptimizationPhase; +use crate::test_utils::LoweringDatabaseForTesting; + +cairo_lang_test_utils::test_file_test!( + reboxing_analysis, + "src/optimizations/test_data", + { + reboxing: "reboxing", + }, + test_reboxing_analysis +); + +fn test_reboxing_analysis( + inputs: &OrderedHashMap, + _args: &OrderedHashMap, +) -> TestRunnerResult { + let db = &mut LoweringDatabaseForTesting::default(); + let (test_function, semantic_diagnostics) = setup_test_function(db, inputs).split(); + + let function_id = + ConcreteFunctionWithBodyId::from_semantic(db, test_function.concrete_function_id); + if let Ok(mut before) = db.lowered_body(function_id, LoweringStage::PreOptimizations).cloned() { + OptimizationPhase::ApplyInlining { enable_const_folding: true } + .apply(db, function_id, &mut before) + .unwrap(); + OptimizationPhase::ReorganizeBlocks.apply(db, function_id, &mut before).unwrap(); + + OptimizationPhase::ReorderStatements.apply(db, function_id, &mut before).unwrap(); + let mut after = before.clone(); + + let formatter = LoweredFormatter::new(db, &after.variables); + trace!("Lowering input to Reboxing:\n{:?}", after.debug(&formatter)); + + let candidates = find_reboxing_candidates(db, &after); + + let candidates_str = candidates + .iter() + .map(|v| format!("v{}", v.reboxed_var.index())) + .collect::>() + .join(", "); + + // Apply reboxing optimizations to create "after" state + apply_reboxing_candidates(db, &mut after, &candidates); + + TestRunnerResult::success(OrderedHashMap::from([ + ("candidates".into(), candidates_str), + ( + "before".into(), + format!("{:?}", before.debug(&LoweredFormatter::new(db, &before.variables))), + ), + ( + "after".into(), + format!("{:?}", after.debug(&LoweredFormatter::new(db, &after.variables))), + ), + ])) + } else { + let lowering_diags = + db.module_lowering_diagnostics(test_function.module_id).map_or("".to_string(), |d| { + d.get_all().iter().map(|d| format!("{d:?}")).collect::>().join("\n") + }); + panic!( + "Unexpected diagnostics:\nSemantic:\n{}\nLowering:\n{}", + semantic_diagnostics, lowering_diags + ); + } +} diff --git a/crates/cairo-lang-lowering/src/optimizations/strategy.rs b/crates/cairo-lang-lowering/src/optimizations/strategy.rs index f97c495b746..d3a17aba82a 100644 --- a/crates/cairo-lang-lowering/src/optimizations/strategy.rs +++ b/crates/cairo-lang-lowering/src/optimizations/strategy.rs @@ -7,6 +7,7 @@ use super::cse::cse; use super::dedup_blocks::dedup_blocks; use super::early_unsafe_panic::early_unsafe_panic; use super::gas_redeposit::gas_redeposit; +use super::reboxing::apply_reboxing; use super::trim_unreachable::trim_unreachable; use super::validate::validate; use crate::Lowered; @@ -39,6 +40,7 @@ pub enum OptimizationPhase<'db> { EarlyUnsafePanic, OptimizeMatches, OptimizeRemappings, + Reboxing, ReorderStatements, ReorganizeBlocks, ReturnOptimization, @@ -84,6 +86,7 @@ impl<'db> OptimizationPhase<'db> { OptimizationPhase::DedupBlocks => dedup_blocks(lowered), OptimizationPhase::OptimizeMatches => optimize_matches(lowered), OptimizationPhase::OptimizeRemappings => optimize_remappings(lowered), + OptimizationPhase::Reboxing => apply_reboxing(db, lowered), OptimizationPhase::ReorderStatements => reorder_statements(db, lowered), OptimizationPhase::ReorganizeBlocks => reorganize_blocks(lowered), OptimizationPhase::ReturnOptimization => return_optimization(db, lowered), @@ -160,6 +163,7 @@ pub fn baseline_optimization_strategy<'db>(db: &'db dyn Database) -> Optimizatio OptimizationPhase::ReorderStatements, OptimizationPhase::OptimizeMatches, OptimizationPhase::ReorganizeBlocks, + OptimizationPhase::Reboxing, OptimizationPhase::CancelOps, OptimizationPhase::ReorganizeBlocks, // Performing CSE here after blocks are the most contiguous, to reach maximum diff --git a/crates/cairo-lang-lowering/src/optimizations/test_data/reboxing b/crates/cairo-lang-lowering/src/optimizations/test_data/reboxing new file mode 100644 index 00000000000..cd7da13cd44 --- /dev/null +++ b/crates/cairo-lang-lowering/src/optimizations/test_data/reboxing @@ -0,0 +1,692 @@ +//! > Test reboxing + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +main + +//! > module_code +#[derive(Drop, Copy)] +struct A { + a: felt252, + b: felt252, +} + +//! > function_code +fn main(a: Box) -> Box { + BoxTrait::new(a.b) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates +v4 + +//! > before +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::A) <- core::box::unbox::(v0) + (v2: core::felt252, v3: core::felt252) <- struct_destructure(v1) + (v4: core::box::Box::) <- core::box::into_box::(v3) +End: + Return(v4) + +//! > after +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::A) <- core::box::unbox::(v0) + (v2: core::felt252, v3: core::felt252) <- struct_destructure(v1) + (v5: core::box::Box::, v4: core::box::Box::) <- struct_destructure(v0) +End: + Return(v4) + +//! > ========================================================================== + +//! > Test reboxing with multiple members + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +multi_member + +//! > TODO(eytan-starkware): Support multiple members by unboxing each member separately. + +//! > module_code +#[derive(Drop, Copy)] +struct Point { + x: u32, + y: u32, + z: u32, +} + +//! > function_code +fn multi_member(p: Box) -> (Box, Box) { + (BoxTrait::new(p.x), BoxTrait::new(p.z)) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates +v5, v10 + +//! > before +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::Point) <- core::box::unbox::(v0) + (v2: core::integer::u32, v3: core::integer::u32, v4: core::integer::u32) <- struct_destructure(v1) + (v5: core::box::Box::) <- core::box::into_box::(v2) + (v6: test::Point) <- core::box::unbox::(v0) + (v7: core::integer::u32, v8: core::integer::u32, v9: core::integer::u32) <- struct_destructure(v6) + (v10: core::box::Box::) <- core::box::into_box::(v9) + (v11: (core::box::Box::, core::box::Box::)) <- struct_construct(v5, v10) +End: + Return(v11) + +//! > after +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::Point) <- core::box::unbox::(v0) + (v2: core::integer::u32, v3: core::integer::u32, v4: core::integer::u32) <- struct_destructure(v1) + (v5: core::box::Box::, v12: core::box::Box::, v13: core::box::Box::) <- struct_destructure(v0) + (v6: test::Point) <- core::box::unbox::(v0) + (v7: core::integer::u32, v8: core::integer::u32, v9: core::integer::u32) <- struct_destructure(v6) + (v14: core::box::Box::, v15: core::box::Box::, v10: core::box::Box::) <- struct_destructure(v0) + (v11: (core::box::Box::, core::box::Box::)) <- struct_construct(v5, v10) +End: + Return(v11) + +//! > ========================================================================== + +//! > Test reboxing with nested struct + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +nested_struct + +//! > TODO(eytan-starkware): Add nesting support + +//! > module_code +#[derive(Drop, Copy)] +struct Inner { + value: felt252, +} + +#[derive(Drop, Copy)] +struct Outer { + inner: Inner, + other: felt252, +} + +//! > function_code +fn nested_struct(o: Box) -> Box { + BoxTrait::new(o.inner.value) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates +v5 + +//! > before +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::Outer) <- core::box::unbox::(v0) + (v2: test::Inner, v3: core::felt252) <- struct_destructure(v1) + (v4: core::felt252) <- struct_destructure(v2) + (v5: core::box::Box::) <- core::box::into_box::(v4) +End: + Return(v5) + +//! > after +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::Outer) <- core::box::unbox::(v0) + (v2: test::Inner, v3: core::felt252) <- struct_destructure(v1) + (v4: core::felt252) <- struct_destructure(v2) + (v5: core::box::Box::) <- core::box::into_box::(v4) +End: + Return(v5) + +//! > ========================================================================== + +//! > Test reboxing with snapshot struct + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +with_snapshot + +//! > TODO(eytan-starkware): Add snapshot support + +//! > module_code +#[derive(Drop)] +struct Data { + non_copy: NonCopy, + b: felt252, +} + +#[derive(Drop)] +struct NonCopy { + a: felt252, +} + +//! > function_code +fn with_snapshot(data: Box<@Data>) -> Box<@NonCopy> { + BoxTrait::new(data.unbox().non_copy) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates +v4 + +//! > before +Parameters: v0: core::box::Box::<@test::Data> +blk0 (root): +Statements: + (v1: @test::Data) <- core::box::unbox::<@test::Data>(v0) + (v2: @test::NonCopy, v3: @core::felt252) <- struct_destructure(v1) + (v4: core::box::Box::<@test::NonCopy>) <- core::box::into_box::<@test::NonCopy>(v2) +End: + Return(v4) + +//! > after +Parameters: v0: core::box::Box::<@test::Data> +blk0 (root): +Statements: + (v1: @test::Data) <- core::box::unbox::<@test::Data>(v0) + (v2: @test::NonCopy, v3: @core::felt252) <- struct_destructure(v1) + (v4: core::box::Box::<@test::NonCopy>) <- core::box::into_box::<@test::NonCopy>(v2) +End: + Return(v4) + +//! > ========================================================================== + +//! > Test reboxing without destructure (rebox whole struct) + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +rebox_whole + +//! > TODO(eytan-starkware): Add support for whole var reboxing + +//! > module_code +struct Simple { + value: felt252, +} + +//! > function_code +fn rebox_whole(s: Box) -> Box { + BoxTrait::new(s.unbox()) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates +v2 + +//! > before +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::Simple) <- core::box::unbox::(v0) + (v2: core::box::Box::) <- core::box::into_box::(v1) +End: + Return(v2) + +//! > after +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::Simple) <- core::box::unbox::(v0) + (v2: core::box::Box::) <- core::box::into_box::(v1) +End: + Return(v2) + +//! > ========================================================================== + +//! > Test no variable confusion in reboxing analysis + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +no_opportunity + +//! > module_code +#[derive(Drop, Copy)] +struct Data { + a: felt252, + b: felt252, +} + +//! > function_code +fn no_opportunity(d: Box, x: felt252) -> Box { + let _unused = d.a; + BoxTrait::new(x) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates + +//! > before +Parameters: v0: core::box::Box::, v1: core::felt252 +blk0 (root): +Statements: + (v5: core::box::Box::) <- core::box::into_box::(v1) +End: + Return(v5) + +//! > after +Parameters: v0: core::box::Box::, v1: core::felt252 +blk0 (root): +Statements: + (v5: core::box::Box::) <- core::box::into_box::(v1) +End: + Return(v5) + +//! > ========================================================================== + +//! > Test reboxing with enum + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +enum_rebox + +//! > module_code +#[derive(Drop, Copy)] +enum MyEnum { + A: u32, + B: u32, +} + +//! > function_code +fn enum_rebox(e: Box) -> Box { + let a = match e.unbox() { + MyEnum::A(x) => x, + MyEnum::B(x) => x, + }; + BoxTrait::new(a) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates + +//! > before +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::MyEnum) <- core::box::unbox::(v0) +End: + Match(match_enum(v1) { + MyEnum::A(v2) => blk1, + MyEnum::B(v3) => blk2, + }) + +blk1: +Statements: +End: + Goto(blk3, {v2 -> v4}) + +blk2: +Statements: +End: + Goto(blk3, {v3 -> v4}) + +blk3: +Statements: + (v5: core::box::Box::) <- core::box::into_box::(v4) +End: + Return(v5) + +//! > after +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::MyEnum) <- core::box::unbox::(v0) +End: + Match(match_enum(v1) { + MyEnum::A(v2) => blk1, + MyEnum::B(v3) => blk2, + }) + +blk1: +Statements: +End: + Goto(blk3, {v2 -> v4}) + +blk2: +Statements: +End: + Goto(blk3, {v3 -> v4}) + +blk3: +Statements: + (v5: core::box::Box::) <- core::box::into_box::(v4) +End: + Return(v5) + +//! > ========================================================================== + +//! > Test reboxing with tuple + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +tuple_rebox + +//! > module_code + +//! > function_code +fn tuple_rebox(t: Box<(u32, felt252, u64)>) -> Box { + let (_, mid, _) = t.unbox(); + BoxTrait::new(mid) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates +v5 + +//! > before +Parameters: v0: core::box::Box::<(core::integer::u32, core::felt252, core::integer::u64)> +blk0 (root): +Statements: + (v1: (core::integer::u32, core::felt252, core::integer::u64)) <- core::box::unbox::<(core::integer::u32, core::felt252, core::integer::u64)>(v0) + (v2: core::integer::u32, v3: core::felt252, v4: core::integer::u64) <- struct_destructure(v1) + (v5: core::box::Box::) <- core::box::into_box::(v3) +End: + Return(v5) + +//! > after +Parameters: v0: core::box::Box::<(core::integer::u32, core::felt252, core::integer::u64)> +blk0 (root): +Statements: + (v1: (core::integer::u32, core::felt252, core::integer::u64)) <- core::box::unbox::<(core::integer::u32, core::felt252, core::integer::u64)>(v0) + (v2: core::integer::u32, v3: core::felt252, v4: core::integer::u64) <- struct_destructure(v1) + (v6: core::box::Box::, v5: core::box::Box::, v7: core::box::Box::) <- struct_destructure(v0) +End: + Return(v5) + +//! > ========================================================================== + +//! > Test reboxing with non-Copy struct + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +non_copy_struct_rebox + +//! > TODO(eytan-starkware): Add support for non-copy structs where applicable + +//! > module_code +#[derive(Drop)] +struct NonCopyStruct { + arr: Array, + val: felt252, +} + +//! > function_code +fn non_copy_struct_rebox(s: Box) -> Box { + BoxTrait::new(s.unbox().val) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates +v4 + +//! > before +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::NonCopyStruct) <- core::box::unbox::(v0) + (v2: core::array::Array::, v3: core::felt252) <- struct_destructure(v1) + (v4: core::box::Box::) <- core::box::into_box::(v3) +End: + Return(v4) + +//! > after +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::NonCopyStruct) <- core::box::unbox::(v0) + (v2: core::array::Array::, v3: core::felt252) <- struct_destructure(v1) + (v4: core::box::Box::) <- core::box::into_box::(v3) +End: + Return(v4) + +//! > ========================================================================== + +//! > Test reboxing with non-Copy tuple + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +non_copy_tuple_rebox + +//! > module_code + +//! > function_code +fn non_copy_tuple_rebox(t: Box<(Array, felt252)>) -> Box { + let (_, val) = t.unbox(); + BoxTrait::new(val) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates +v4 + +//! > before +Parameters: v0: core::box::Box::<(core::array::Array::, core::felt252)> +blk0 (root): +Statements: + (v1: (core::array::Array::, core::felt252)) <- core::box::unbox::<(core::array::Array::, core::felt252)>(v0) + (v2: core::array::Array::, v3: core::felt252) <- struct_destructure(v1) + (v4: core::box::Box::) <- core::box::into_box::(v3) +End: + Return(v4) + +//! > after +Parameters: v0: core::box::Box::<(core::array::Array::, core::felt252)> +blk0 (root): +Statements: + (v1: (core::array::Array::, core::felt252)) <- core::box::unbox::<(core::array::Array::, core::felt252)>(v0) + (v2: core::array::Array::, v3: core::felt252) <- struct_destructure(v1) + (v4: core::box::Box::) <- core::box::into_box::(v3) +End: + Return(v4) + +//! > ========================================================================== + +//! > Test reboxing with branch-specific unbox assignments + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +branch_mut_unbox + +//! > TODO(eytan-starkware): Support equality tracking and replacing the original deconstruct statement + +//! > module_code +#[derive(Drop, Copy)] +struct Pair { + left: felt252, + right: felt252, +} + +//! > function_code +fn branch_mut_unbox(flag: bool, p1: Box, p2: Box) -> Box { + let mut x = p1.unbox(); + if flag { + x = p1.unbox(); + } else { + x = p2.unbox(); + } + BoxTrait::new(x.right) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates + +//! > before +Parameters: v0: core::bool, v1: core::box::Box::, v2: core::box::Box:: +blk0 (root): +Statements: +End: + Match(match_enum(v0) { + bool::False(v4) => blk1, + bool::True(v5) => blk2, + }) + +blk1: +Statements: + (v6: test::Pair) <- core::box::unbox::(v2) +End: + Goto(blk3, {v6 -> v7}) + +blk2: +Statements: + (v8: test::Pair) <- core::box::unbox::(v1) +End: + Goto(blk3, {v8 -> v7}) + +blk3: +Statements: + (v9: core::felt252, v10: core::felt252) <- struct_destructure(v7) + (v11: core::box::Box::) <- core::box::into_box::(v10) +End: + Return(v11) + +//! > after +Parameters: v0: core::bool, v1: core::box::Box::, v2: core::box::Box:: +blk0 (root): +Statements: +End: + Match(match_enum(v0) { + bool::False(v4) => blk1, + bool::True(v5) => blk2, + }) + +blk1: +Statements: + (v6: test::Pair) <- core::box::unbox::(v2) +End: + Goto(blk3, {v6 -> v7}) + +blk2: +Statements: + (v8: test::Pair) <- core::box::unbox::(v1) +End: + Goto(blk3, {v8 -> v7}) + +blk3: +Statements: + (v9: core::felt252, v10: core::felt252) <- struct_destructure(v7) + (v11: core::box::Box::) <- core::box::into_box::(v10) +End: + Return(v11) + +//! > ========================================================================== + +//! > Test reboxing with non-drop struct and member + +//! > test_runner_name +test_reboxing_analysis + +//! > function_name +main + +//! > TODO(eytan-starkware): Support non-drop by unboxing each member. + +//! > module_code +#[derive(Copy)] +struct NonDrop { + b: felt252, +} + +#[derive(Copy)] +struct A { + a: felt252, + non_drop: NonDrop, +} + +//! > function_code +fn main(a: Box) -> (Box, NonDrop) { + let unboxed = a.unbox(); + (BoxTrait::new(unboxed.a), unboxed.non_drop) +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > candidates +v4 + +//! > before +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::A) <- core::box::unbox::(v0) + (v2: core::felt252, v3: test::NonDrop) <- struct_destructure(v1) + (v4: core::box::Box::) <- core::box::into_box::(v2) + (v5: (core::box::Box::, test::NonDrop)) <- struct_construct(v4, v3) +End: + Return(v5) + +//! > after +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: test::A) <- core::box::unbox::(v0) + (v2: core::felt252, v3: test::NonDrop) <- struct_destructure(v1) + (v4: core::box::Box::) <- core::box::into_box::(v2) + (v5: (core::box::Box::, test::NonDrop)) <- struct_construct(v4, v3) +End: + Return(v5) diff --git a/crates/cairo-lang-sierra-generator/src/function_generator_test_data/struct b/crates/cairo-lang-sierra-generator/src/function_generator_test_data/struct index 09a2f067e59..aaed758a173 100644 --- a/crates/cairo-lang-sierra-generator/src/function_generator_test_data/struct +++ b/crates/cairo-lang-sierra-generator/src/function_generator_test_data/struct @@ -25,3 +25,145 @@ struct_deconstruct([1]) -> ([2], [3]) drop([2]) -> () store_temp([3]) -> ([3]) return([3]) + +//! > ========================================================================== + +//! > Test that boxing a member of a boxed struct does not do any copying. + +//! > test_runner_name +test_function_generator(future_sierra:true) + +//! > function_code +fn foo(a: Box) -> Box { + let val: u256 = a.unbox(); + BoxTrait::new(val.high) +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > sierra_gen_diagnostics + +//! > sierra_code +label_test::foo::0: +dup>([0]) -> ([0], [1]) +unbox([1]) -> ([2]) +drop([2]) -> () +struct_boxed_deconstruct([0]) -> ([3], [4]) +drop>([3]) -> () +store_temp>([4]) -> ([4]) +return([4]) + +//! > ========================================================================== + +//! > Test reboxing if a member of a snaphosted boxed struct. + +//! > test_runner_name +test_function_generator(future_sierra:true) + +//! > function_code +fn foo(a: @Box) -> Box { + BoxTrait::new(a.unbox().high) +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > sierra_code +label_test::foo::0: +rename>([0]) -> ([1]) +dup>([1]) -> ([1], [2]) +unbox([2]) -> ([3]) +drop([3]) -> () +struct_boxed_deconstruct([1]) -> ([4], [5]) +drop>([4]) -> () +store_temp>([5]) -> ([5]) +return([5]) + +//! > ========================================================================== + +//! > Test reboxing of a non-copyable member of a snapshotted boxed struct. + +//! > test_runner_name +test_function_generator(future_sierra:true) + +//! > function_code +fn foo(box: Box<@A>) -> Box<@Array> { + BoxTrait::new(box.unbox().a) +} + +//! > function_name +foo + +//! > module_code +#[derive(Drop)] +struct A { + a: Array, + b: Array, +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > TODO(eytan-starkware): Rebox on non-copy structs. + +//! > sierra_code +label_test::foo::0: +unbox>([0]) -> ([1]) +store_temp>([1]) -> ([1]) +struct_snapshot_deconstruct([1]) -> ([2], [3]) +drop>>([3]) -> () +into_box>>([2]) -> ([4]) +return([4]) + +//! > ========================================================================== + +//! > Test reboxing of a boxed snapshotted struct. + +//! > test_runner_name +test_function_generator(future_sierra:true) + +//! > function_code +fn foo(box: Box<@A>) -> Box<@Array> { + BoxTrait::new(box.unbox().a) +} + +//! > function_name +foo + +//! > module_code +#[derive(Drop)] +struct A { + a: Array, + b: Array, +} + +//! > semantic_diagnostics + +//! > lowering_diagnostics + +//! > TODO(eytan-starkware): We want reboxing to apply to sierra in the future, + +//! > so we will see struct_boxed_deconstruct. + +//! > sierra_code +label_test::foo::0: +unbox>([0]) -> ([1]) +store_temp>([1]) -> ([1]) +struct_snapshot_deconstruct([1]) -> ([2], [3]) +drop>>([3]) -> () +into_box>>([2]) -> ([4]) +return([4]) diff --git a/crates/cairo-lang-sierra-generator/src/function_generator_test_utils.rs b/crates/cairo-lang-sierra-generator/src/function_generator_test_utils.rs index 330edb8d70e..e672aff031f 100644 --- a/crates/cairo-lang-sierra-generator/src/function_generator_test_utils.rs +++ b/crates/cairo-lang-sierra-generator/src/function_generator_test_utils.rs @@ -1,3 +1,8 @@ +use std::sync::Arc; + +use cairo_lang_filesystem::db::FilesGroup; +use cairo_lang_filesystem::flag::Flag; +use cairo_lang_filesystem::ids::FlagLongId; use cairo_lang_lowering::db::LoweringGroup; use cairo_lang_lowering::ids::ConcreteFunctionWithBodyId; use cairo_lang_semantic::test_utils::setup_test_function; @@ -11,12 +16,27 @@ use crate::test_utils::SierraGenDatabaseForTesting; /// Compiles a single function to Sierra and checks the generated code. pub fn test_function_generator( inputs: &OrderedHashMap, - _args: &OrderedHashMap, + args: &OrderedHashMap, ) -> TestRunnerResult { // Tests have recursions for revoking AP. Automatic addition of 'withdraw_gas` calls would add // unnecessary complication to them. - let db = &SierraGenDatabaseForTesting::without_add_withdraw_gas(); + let db = if let Some(v) = args.get("future_sierra").map(|s| s.to_lowercase()) + && v == "true" + { + // When turning on future_sierra, we might affect other tests using the same db, so an empty + // db is needed. + let mut db = SierraGenDatabaseForTesting::new_empty(); + db.set_flag( + FlagLongId("add_withdraw_gas".into()), + Some(Arc::new(Flag::AddWithdrawGas(false))), + ); + db.set_flag(FlagLongId("future_sierra".into()), Some(Arc::new(Flag::FutureSierra(true)))); + db + } else { + SierraGenDatabaseForTesting::without_add_withdraw_gas() + }; + let db = &db; // Parse code and create semantic model. let (test_function, semantic_diagnostics) = setup_test_function(db, inputs).split(); diff --git a/crates/cairo-lang-sierra-generator/src/utils.rs b/crates/cairo-lang-sierra-generator/src/utils.rs index 38ee9afc365..b1c0f6e2b5d 100644 --- a/crates/cairo-lang-sierra-generator/src/utils.rs +++ b/crates/cairo-lang-sierra-generator/src/utils.rs @@ -8,6 +8,7 @@ use cairo_lang_sierra::extensions::const_type::{ }; use cairo_lang_sierra::extensions::core::CoreLibfunc; use cairo_lang_sierra::extensions::lib_func::LibfuncSignature; +use cairo_lang_sierra::extensions::modules::boxing::BoxType; use cairo_lang_sierra::extensions::snapshot::SnapshotType; use cairo_lang_sierra::extensions::{ ExtensionError, GenericLibfuncEx, NamedLibfunc, NamedType, SpecializationError, @@ -109,10 +110,14 @@ pub fn struct_deconstruct_libfunc_id( ) -> Maybe { let long_id = &db.get_type_info(ty.clone())?.long_id; let is_snapshot = long_id.generic_id == SnapshotType::id(); + let is_box = long_id.generic_id == BoxType::id(); Ok(if is_snapshot { let concrete_enum_type = extract_matches!(&long_id.generic_args[0], GenericArg::Type).clone(); get_libfunc_id_with_generic_arg(db, "struct_snapshot_deconstruct", concrete_enum_type) + } else if is_box { + let inner_ty = extract_matches!(&long_id.generic_args[0], GenericArg::Type).clone(); + get_libfunc_id_with_generic_arg(db, "struct_boxed_deconstruct", inner_ty) } else { get_libfunc_id_with_generic_arg(db, "struct_deconstruct", ty) })