From c70c62ef282f583dba5d203ead4385a00245328f Mon Sep 17 00:00:00 2001 From: Eytan Singher Date: Sun, 1 Feb 2026 16:12:48 +0200 Subject: [PATCH] feat: Dataflow forward equality analysis --- .../src/analysis/equality_analysis.rs | 434 ++++++++++++ .../src/analysis/equality_analysis_test.rs | 58 ++ .../cairo-lang-lowering/src/analysis/mod.rs | 3 + .../src/analysis/test_data/equality | 627 ++++++++++++++++++ 4 files changed, 1122 insertions(+) create mode 100644 crates/cairo-lang-lowering/src/analysis/equality_analysis.rs create mode 100644 crates/cairo-lang-lowering/src/analysis/equality_analysis_test.rs create mode 100644 crates/cairo-lang-lowering/src/analysis/test_data/equality diff --git a/crates/cairo-lang-lowering/src/analysis/equality_analysis.rs b/crates/cairo-lang-lowering/src/analysis/equality_analysis.rs new file mode 100644 index 00000000000..65e0a2ba7ea --- /dev/null +++ b/crates/cairo-lang-lowering/src/analysis/equality_analysis.rs @@ -0,0 +1,434 @@ +//! Equality analysis for lowered IR. +//! +//! This module tracks semantic equivalence between variables as information flows through the +//! program. Two variables are equivalent if they hold the same value. Additionally, the analysis +//! tracks `Box`/unbox and snapshot/desnap relationships between equivalence classes. + +use std::fmt; + +use cairo_lang_utils::ordered_hash_map::OrderedHashMap; + +use crate::analysis::core::Edge; +use crate::analysis::{DataflowAnalyzer, Direction, ForwardDataflowAnalysis}; +use crate::{BlockEnd, BlockId, Lowered, Statement, VariableId}; + +/// Tracks relationships between equivalence classes. +#[derive(Clone, Debug, Default)] +struct ClassInfo { + /// If this class has a boxed version, the representative of that class. + boxed_class: Option, + /// If this class has an unboxed version, the representative of that class. + unboxed_class: Option, + /// If this class has a snapshot version, the representative of that class. + snapshot_class: Option, + /// If this class is a snapshot, the representative of the original class. + original_class: Option, +} + +impl ClassInfo { + /// Returns all variables referenced by this ClassInfo's relationships. + fn referenced_vars(&self) -> impl Iterator + '_ { + self.boxed_class + .iter() + .chain(self.original_class.iter()) + .chain(self.snapshot_class.iter()) + .chain(self.unboxed_class.iter()) + .copied() + } + + /// Returns true if this ClassInfo has no relationships. + fn is_empty(&self) -> bool { + self.boxed_class.is_none() + && self.unboxed_class.is_none() + && self.snapshot_class.is_none() + && self.original_class.is_none() + } + + /// Merges another ClassInfo into this one. + /// When both have the same relationship type, calls union_fn to merge the related classes. + fn merge( + self, + other: Self, + union_fn: &mut impl FnMut(VariableId, VariableId) -> VariableId, + ) -> Self { + let mut merge_field = |new: Option, old: Option| match (new, old) { + (Some(new_val), Some(old_val)) if new_val != old_val => { + Some(union_fn(new_val, old_val)) + } + (new, old) => new.or(old), + }; + Self { + boxed_class: merge_field(self.boxed_class, other.boxed_class), + unboxed_class: merge_field(self.unboxed_class, other.unboxed_class), + snapshot_class: merge_field(self.snapshot_class, other.snapshot_class), + original_class: merge_field(self.original_class, other.original_class), + } + } +} + +/// State for the equality analysis, tracking variable equivalences. +/// +/// This is the `Info` type for the dataflow analysis. Each block gets its own +/// `EqualityState` representing what we know at that point in the program. +/// +/// Uses sparse HashMaps for efficiency - only variables that have been touched +/// by the analysis are stored. +#[derive(Clone, Default)] +pub struct EqualityState { + /// Union-find parent map. If a variable is not in the map, it is its own representative. + union_find: OrderedHashMap, + + /// For each equivalence class representative, track relationships only if they exist. + class_info: OrderedHashMap, +} + +impl EqualityState { + /// Creates a new empty equality state. + pub fn new(_lowered: &Lowered<'_>) -> Self { + Self::default() + } + + /// Gets the parent of a variable, defaulting to itself (root) if not in the map. + fn get_parent(&self, var: VariableId) -> VariableId { + self.union_find.get(&var).copied().unwrap_or(var) + } + + /// Gets the class info for a variable, returning a default if not present. + fn get_class_info(&self, var: VariableId) -> ClassInfo { + self.class_info.get(&var).cloned().unwrap_or_default() + } + + /// Finds the representative of a variable's equivalence class. + /// Uses path compression for efficiency. + fn find(&mut self, var: VariableId) -> VariableId { + let parent = self.get_parent(var); + if parent != var { + let root = self.find(parent); + // Path compression: point directly to root. + self.union_find.insert(var, root); + root + } else { + var + } + } + + /// Finds the representative without modifying the structure. + fn find_immut(&self, var: VariableId) -> VariableId { + let parent = self.get_parent(var); + if parent != var { self.find_immut(parent) } else { var } + } + + /// Unions two variables into the same equivalence class. + /// Returns the representative of the merged class. + /// Always chooses the lower ID as the representative to maintain canonical form. + fn union(&mut self, a: VariableId, b: VariableId) -> VariableId { + let root_a = self.find(a); + let root_b = self.find(b); + + if root_a == root_b { + return root_a; + } + + // Always choose the lower ID as the new root to maintain canonical form. + // This ensures hashcons keys remain valid since lower IDs are defined earlier. + let (new_root, old_root) = + if root_a.index() < root_b.index() { (root_a, root_b) } else { (root_b, root_a) }; + + // Ensure new_root is in the map (as its own parent). + self.union_find.entry(new_root).or_insert(new_root); + // Update old_root to point to new_root. + self.union_find.insert(old_root, new_root); + + // Merge class info: since A == B, we have Box(A) == Box(B), @A == @B, etc. + // Recursive unions inside merge() only affect related classes (which have strictly + // one-step increment in information in forward analysis), so they never deposit class_info + // back at new_root. + let old_info = self.class_info.swap_remove(&old_root).unwrap_or_default(); + let new_info = self.class_info.swap_remove(&new_root).unwrap_or_default(); + let merged = new_info.merge(old_info, &mut |a, b| self.union(a, b)); + if !merged.is_empty() { + let final_root = self.find(new_root); + self.class_info.insert(final_root, merged); + } + + self.find(new_root) + } + + /// Looks up a related variable through a ClassInfo field accessor. + fn get_related( + &mut self, + var: VariableId, + field: fn(&mut ClassInfo) -> &mut Option, + ) -> Option { + let rep = self.find(var); + let mut info = self.get_class_info(rep); + let related = (*field(&mut info))?; + Some(self.find(related)) + } + + /// Sets a bidirectional relationship between two variables' equivalence classes. + /// If inputs already have a relationship of the same kind, unions with the existing class. + fn set_relationship( + &mut self, + var_a: VariableId, + var_b: VariableId, + field_a_to_b: fn(&mut ClassInfo) -> &mut Option, + field_b_to_a: fn(&mut ClassInfo) -> &mut Option, + ) { + // Union with existing relationships if present. + if let Some(existing) = self.get_related(var_a, field_a_to_b) { + self.union(var_b, existing); + } + if let Some(existing) = self.get_related(var_b, field_b_to_a) { + self.union(var_a, existing); + } + + // Re-find after potential unions — representatives may have changed. + let rep_a = self.find(var_a); + let rep_b = self.find(var_b); + + *field_a_to_b(self.class_info.entry(rep_a).or_default()) = Some(rep_b); + *field_b_to_a(self.class_info.entry(rep_b).or_default()) = Some(rep_a); + } + + /// Sets a box relationship: boxed_var = Box(unboxed_var). + fn set_box_relationship(&mut self, unboxed_var: VariableId, boxed_var: VariableId) { + self.set_relationship( + unboxed_var, + boxed_var, + |ci| &mut ci.boxed_class, + |ci| &mut ci.unboxed_class, + ); + } + + /// Sets a snapshot relationship: snapshot_var = @original_var. + fn set_snapshot_relationship(&mut self, original_var: VariableId, snapshot_var: VariableId) { + self.set_relationship( + original_var, + snapshot_var, + |ci| &mut ci.snapshot_class, + |ci| &mut ci.original_class, + ); + } +} + +impl fmt::Debug for EqualityState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let v = |id: VariableId| format!("v{}", self.find_immut(id).index()); + let mut lines = Vec::::new(); + for (&rep, info) in self.class_info.iter() { + if let Some(s) = info.snapshot_class { + lines.push(format!("@{} = {}", v(rep), v(s))); + } + if let Some(b) = info.boxed_class { + lines.push(format!("Box({}) = {}", v(rep), v(b))); + } + } + for &var in self.union_find.keys() { + let rep = self.find_immut(var); + if var != rep { + lines.push(format!("v{} = v{}", rep.index(), var.index())); + } + } + lines.sort(); + if lines.is_empty() { write!(f, "(empty)") } else { write!(f, "{}", lines.join(", ")) } + } +} + +/// Variable equality analysis. +/// +/// This analyzer tracks snapshot/desnap and box/unbox relationships as data flows +/// through the program. At merge points (after match arms converge), we conservatively +/// intersect the equivalence classes, keeping only equalities that hold on all paths. +pub struct EqualityAnalysis<'db, 'a> { + lowered: &'a Lowered<'db>, +} + +impl<'db, 'a> EqualityAnalysis<'db, 'a> { + /// Creates a new equality analyzer. + pub fn new(lowered: &'a Lowered<'db>) -> Self { + Self { lowered } + } + + /// Runs equality analysis on a lowered function. + /// Returns the equality state at the exit of each block. + pub fn analyze(lowered: &'a Lowered<'db>) -> Vec> { + ForwardDataflowAnalysis::new(lowered, EqualityAnalysis::new(lowered)).run() + } +} + +/// Returns an iterator over all variables with equality ir relationship information in the equality +/// states. +fn merge_referenced_vars<'a>( + info1: &'a EqualityState, + info2: &'a EqualityState, +) -> impl Iterator + 'a { + let union_find_vars = info1.union_find.keys().chain(info2.union_find.keys()).copied(); + + let class_info_vars = info1 + .class_info + .values() + .chain(info2.class_info.values()) + .flat_map(ClassInfo::referenced_vars); + + union_find_vars.chain(class_info_vars) +} + +/// Preserves only class relationships (box/snapshot) that exist in both branches. +fn merge_class_relationships( + info1: &EqualityState, + info2: &EqualityState, + intersections: &OrderedHashMap>, + result: &mut EqualityState, +) { + /// Finds an intersection representative: given a rep in info1 and a rep in info2, + /// returns the intersection representative in the result if one exists. + fn find_intersection_rep( + equality_state: &mut EqualityState, + info1: &EqualityState, + info2: &EqualityState, + intersections: &OrderedHashMap>, + rep1: Option, + rep2: Option, + ) -> Option { + let (rep1, rep2) = (info1.find_immut(rep1?), info2.find_immut(rep2?)); + intersections.get(&rep1)?.iter().find_map(|(intersection_r2, intersection_rep)| { + (*intersection_r2 == rep2).then(|| equality_state.find(*intersection_rep)) + }) + } + + for (&var, class1) in info1.class_info.iter() { + for &(var_rep2, intersection_var) in intersections.get(&var).unwrap_or(&vec![]) { + let Some(class2) = info2.class_info.get(&var_rep2) else { + continue; + }; + + if let Some(boxed_rep) = find_intersection_rep( + result, + info1, + info2, + intersections, + class1.boxed_class, + class2.boxed_class, + ) { + result.set_box_relationship(intersection_var, boxed_rep); + } + + if let Some(snap_rep) = find_intersection_rep( + result, + info1, + info2, + intersections, + class1.snapshot_class, + class2.snapshot_class, + ) { + result.set_snapshot_relationship(intersection_var, snap_rep); + } + } + } +} + +impl<'db, 'a> DataflowAnalyzer<'db, 'a> for EqualityAnalysis<'db, 'a> { + type Info = EqualityState; + + const DIRECTION: Direction = Direction::Forward; + + fn initial_info(&mut self, _block_id: BlockId, _block_end: &'a BlockEnd<'db>) -> Self::Info { + EqualityState::new(self.lowered) + } + + fn merge( + &mut self, + _lowered: &Lowered<'db>, + _statement_location: super::StatementLocation, + info1: Self::Info, + info2: Self::Info, + ) -> Self::Info { + // Intersection-based merge: keep only equalities that hold in BOTH branches. + let mut result = EqualityState::new(self.lowered); + + // Group variables by (rep1, rep2) - for variables present in either state. + let mut groups: OrderedHashMap<(VariableId, VariableId), Vec> = + OrderedHashMap::default(); + + // Group by (rep1, rep2). Duplicates are fine - they'll just be added to the same group. + for var in merge_referenced_vars(&info1, &info2) { + let key = (info1.find_immut(var), info2.find_immut(var)); + groups.entry(key).or_default().push(var); + } + + // Union all variables within each group + for members in groups.values() { + if members.len() > 1 { + let first = members[0]; + for &var in &members[1..] { + result.union(first, var); + } + } + } + + // An important point in this merge is to retain relationships. + // Consider: + // info1 [equality class[1] = 1, 2, 4] and 6 is Box(1). + // info2 [equality class[2] = 3, 5, 4] and 6 is Box(3). + // To detect we can keep 6 is Box(4), as it is true in all branches, we need intersection of + // eclasses (a single eclass can split in the result into multiple eclasses). + // Build secondary index: rep1 -> Vec<(rep2, intersection_rep)>. + let mut intersections: OrderedHashMap> = + OrderedHashMap::default(); + for (&(rep1, rep2), vars) in groups.iter() { + intersections.entry(rep1).or_default().push((rep2, result.find(vars[0]))); + } + + merge_class_relationships(&info1, &info2, &intersections, &mut result); + + result + } + + fn transfer_stmt( + &mut self, + info: &mut Self::Info, + _statement_location: super::StatementLocation, + stmt: &'a Statement<'db>, + ) { + match stmt { + Statement::Snapshot(snapshot_stmt) => { + info.union(snapshot_stmt.original(), snapshot_stmt.input.var_id); + info.set_snapshot_relationship( + snapshot_stmt.input.var_id, + snapshot_stmt.snapshot(), + ); + } + + Statement::Desnap(desnap_stmt) => { + info.set_snapshot_relationship(desnap_stmt.output, desnap_stmt.input.var_id); + } + + Statement::IntoBox(into_box_stmt) => { + info.set_box_relationship(into_box_stmt.input.var_id, into_box_stmt.output); + } + + Statement::Unbox(unbox_stmt) => { + info.set_box_relationship(unbox_stmt.output, unbox_stmt.input.var_id); + } + + // TODO(eytan-starkware): Struct/enum handling. + Statement::StructConstruct(_) + | Statement::StructDestructure(_) + | Statement::EnumConstruct(_) => {} + + Statement::Const(_) | Statement::Call(_) => {} + } + } + + fn transfer_edge(&mut self, info: &Self::Info, edge: &Edge<'db, 'a>) -> Self::Info { + let mut new_info = info.clone(); + if let Edge::Goto { remapping, .. } = edge { + // Union remapped variables: dst and src should be in the same equivalence class + for (dst, src_usage) in remapping.iter() { + new_info.union(*dst, src_usage.var_id); + } + } + new_info + } +} diff --git a/crates/cairo-lang-lowering/src/analysis/equality_analysis_test.rs b/crates/cairo-lang-lowering/src/analysis/equality_analysis_test.rs new file mode 100644 index 00000000000..75669a82a79 --- /dev/null +++ b/crates/cairo-lang-lowering/src/analysis/equality_analysis_test.rs @@ -0,0 +1,58 @@ +//! File-based tests for the equality analysis. + +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 super::equality_analysis::EqualityAnalysis; +use crate::LoweringStage; +use crate::db::LoweringGroup; +use crate::ids::ConcreteFunctionWithBodyId; +use crate::test_utils::{LoweringDatabaseForTesting, formatted_lowered}; + +cairo_lang_test_utils::test_file_test!( + equality_analysis, + "src/analysis/test_data", + { + equality: "equality", + }, + test_equality_analysis +); + +fn test_equality_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); + + // Use an earlier stage to see the snapshot/box operations before they're optimized away. + let lowered = db.lowered_body(function_id, LoweringStage::PostBaseline); + + let (lowering_str, analysis_state_str) = if let Ok(lowered) = lowered { + let lowering_str = formatted_lowered(db, Some(lowered)); + let block_states = EqualityAnalysis::analyze(lowered); + + // Format each block's state + let analysis_state_str = block_states + .iter() + .enumerate() + .filter_map(|(i, s)| s.as_ref().map(|state| (i, state))) + .map(|(block_idx, state)| format!("Block {block_idx}:\n{state:?}")) + .collect::>() + .join("\n\n"); + + (lowering_str, analysis_state_str) + } else { + ("Lowering failed.".to_string(), "".to_string()) + }; + + TestRunnerResult::success(OrderedHashMap::from([ + ("semantic_diagnostics".into(), semantic_diagnostics), + ("lowering".into(), lowering_str), + ("analysis_state".into(), analysis_state_str), + ])) +} diff --git a/crates/cairo-lang-lowering/src/analysis/mod.rs b/crates/cairo-lang-lowering/src/analysis/mod.rs index 4de5255bf13..e5470bd58ac 100644 --- a/crates/cairo-lang-lowering/src/analysis/mod.rs +++ b/crates/cairo-lang-lowering/src/analysis/mod.rs @@ -5,8 +5,11 @@ pub mod backward; pub mod core; +pub mod equality_analysis; pub mod forward; +#[cfg(test)] +mod equality_analysis_test; #[cfg(test)] mod test; diff --git a/crates/cairo-lang-lowering/src/analysis/test_data/equality b/crates/cairo-lang-lowering/src/analysis/test_data/equality new file mode 100644 index 00000000000..bc237210076 --- /dev/null +++ b/crates/cairo-lang-lowering/src/analysis/test_data/equality @@ -0,0 +1,627 @@ +//! > Test multiple snapshots of same variable creates equivalences + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(x: Array) -> (@Array, @Array) { + let snap1 = @x; + let snap2 = @x; + (snap1, snap2) +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::array::Array:: +blk0 (root): +Statements: + (v1: core::array::Array::, v2: @core::array::Array::) <- snapshot(v0) + (v3: core::array::Array::, v4: @core::array::Array::) <- snapshot(v1) + (v5: (@core::array::Array::, @core::array::Array::)) <- struct_construct(v2, v4) +End: + Return(v5) + +//! > analysis_state +Block 0: +@v0 = v2, v0 = v1, v0 = v3, v2 = v4 + +//! > ========================================================================== + +//! > Test simple function with no equality operations + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(x: felt252, y: felt252) -> felt252 { + x + y +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::felt252, v1: core::felt252 +blk0 (root): +Statements: + (v2: core::felt252) <- core::felt252_add(v0, v1) +End: + Return(v2) + +//! > analysis_state +Block 0: +(empty) + +//! > ========================================================================== + +//! > Test chained snapshots track transitive equality + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(x: Array) -> (@Array, @Array, @Array) { + let snap1 = @x; + let snap2 = @x; + let snap3 = @x; + (snap1, snap2, snap3) +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::array::Array:: +blk0 (root): +Statements: + (v1: core::array::Array::, v2: @core::array::Array::) <- snapshot(v0) + (v3: core::array::Array::, v4: @core::array::Array::) <- snapshot(v1) + (v5: core::array::Array::, v6: @core::array::Array::) <- snapshot(v3) + (v7: (@core::array::Array::, @core::array::Array::, @core::array::Array::)) <- struct_construct(v2, v4, v6) +End: + Return(v7) + +//! > analysis_state +Block 0: +@v0 = v2, v0 = v1, v0 = v3, v0 = v5, v2 = v4, v2 = v6 + +//! > ========================================================================== + +//! > Test box of same value creates box equivalence + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(x: felt252) -> (Box, Box) { + let box1 = BoxTrait::new(x); + let box2 = BoxTrait::new(x); + (box1, box2) +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::felt252 +blk0 (root): +Statements: + (v1: core::box::Box::) <- into_box(v0) + (v2: core::box::Box::) <- into_box(v0) + (v3: (core::box::Box::, core::box::Box::)) <- struct_construct(v1, v2) +End: + Return(v3) + +//! > analysis_state +Block 0: +Box(v0) = v1, v1 = v2 + +//! > ========================================================================== + +//! > Test struct construct and destructure tracks equality + +//! > TODO(eytan-starkware): Track struct field equality through construct/destructure + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(a: felt252, b: felt252) -> (MyStruct, felt252, felt252) { + let s = MyStruct { x: a, y: b }; + let MyStruct { x, y } = s; + (MyStruct { x, y }, x, y) +} + +//! > function_name +foo + +//! > module_code +#[derive(Drop, Copy)] +struct MyStruct { + x: felt252, + y: felt252, +} + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::felt252, v1: core::felt252 +blk0 (root): +Statements: + (v2: test::MyStruct) <- struct_construct(v0, v1) + (v3: (test::MyStruct, core::felt252, core::felt252)) <- struct_construct(v2, v0, v1) +End: + Return(v3) + +//! > analysis_state +Block 0: +(empty) + +//! > ========================================================================== + +//! > Test enum construct tracks variant + +//! > TODO(eytan-starkware): Track enum variant and inner value relationship + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(x: felt252) -> MyEnum { + MyEnum::A(x) +} + +//! > function_name +foo + +//! > module_code +#[derive(Drop)] +enum MyEnum { + A: felt252, + B: felt252, +} + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::felt252 +blk0 (root): +Statements: + (v1: test::MyEnum) <- MyEnum::A(v0) +End: + Return(v1) + +//! > analysis_state +Block 0: +(empty) + +//! > ========================================================================== + +//! > Test match with diamond control flow loses equality info + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(x: felt252) -> felt252 { + let snap1 = @x; + let result = match x { + 0 => 1, + _ => 2, + }; + result +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics +warning[E0001]: Unused variable. Consider ignoring by prefixing with `_`. + --> lib.cairo:2:9 + let snap1 = @x; + ^^^^^ + +//! > lowering +Parameters: v0: core::felt252 +blk0 (root): +Statements: +End: + Match(match core::felt252_is_zero(v0) { + IsZeroResult::Zero => blk1, + IsZeroResult::NonZero(v1) => blk2, + }) + +blk1: +Statements: + (v2: core::felt252) <- 1 +End: + Return(v2) + +blk2: +Statements: + (v3: core::felt252) <- 2 +End: + Return(v3) + +//! > analysis_state +Block 0: +(empty) + +Block 1: +(empty) + +Block 2: +(empty) + +//! > ========================================================================== + +//! > Test match on Option with diamond merges conservatively + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(opt: Option) -> felt252 { + match opt { + Option::Some(val) => val, + Option::None => 0, + } +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::option::Option:: +blk0 (root): +Statements: +End: + Match(match_enum(v0) { + Option::Some(v1) => blk1, + Option::None(v2) => blk2, + }) + +blk1: +Statements: +End: + Return(v1) + +blk2: +Statements: + (v3: core::felt252) <- 0 +End: + Return(v3) + +//! > analysis_state +Block 0: +(empty) + +Block 1: +(empty) + +Block 2: +(empty) + +//! > ========================================================================== + +//! > Test equality preserved within single match arm + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(arr: Array) -> @Array { + match arr.len() { + 0 => { + let snap = @arr; + snap + }, + _ => { + let snap = @arr; + snap + }, + } +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::array::Array:: +blk0 (root): +Statements: + (v1: core::array::Array::, v2: @core::array::Array::) <- snapshot(v0) + (v3: core::integer::u32) <- core::array::array_len::(v2) + (v4: core::felt252) <- core::internal::bounded_int::upcast::(v3) +End: + Match(match core::felt252_is_zero(v4) { + IsZeroResult::Zero => blk1, + IsZeroResult::NonZero(v5) => blk2, + }) + +blk1: +Statements: +End: + Goto(blk3, {}) + +blk2: +Statements: +End: + Goto(blk3, {}) + +blk3: +Statements: + (v6: core::array::Array::, v7: @core::array::Array::) <- snapshot(v1) +End: + Return(v7) + +//! > analysis_state +Block 0: +@v0 = v2, v0 = v1 + +Block 1: +@v0 = v2, v0 = v1 + +Block 2: +@v0 = v2, v0 = v1 + +Block 3: +@v0 = v2, v0 = v1, v0 = v6, v2 = v7 + +//! > ========================================================================== + +//! > Test nested box operations + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(x: felt252) -> (Box>, felt252) { + let box1 = BoxTrait::new(x); + let box2 = BoxTrait::new(box1); + let unbox1 = box2.unbox(); + let unbox2 = unbox1.unbox(); + // Return both to prevent optimization from eliminating the boxes + (box2, unbox2) +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::felt252 +blk0 (root): +Statements: + (v1: core::box::Box::) <- into_box(v0) + (v2: core::box::Box::>) <- into_box(v1) + (v3: (core::box::Box::>, core::felt252)) <- struct_construct(v2, v0) +End: + Return(v3) + +//! > analysis_state +Block 0: +Box(v0) = v1, Box(v1) = v2 + +//! > ========================================================================== + +//! > Test snapshot of boxed value + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(x: Array) -> @Box<@Array> { + let snap = @x; + let boxed = BoxTrait::new(snap); + let box_snap = @boxed; + box_snap +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::array::Array:: +blk0 (root): +Statements: + (v1: core::array::Array::, v2: @core::array::Array::) <- snapshot(v0) + (v3: core::box::Box::<@core::array::Array::>) <- into_box(v2) + (v4: core::box::Box::<@core::array::Array::>, v5: @core::box::Box::<@core::array::Array::>) <- snapshot(v3) +End: + Return(v5) + +//! > analysis_state +Block 0: +@v0 = v2, @v3 = v5, Box(v2) = v3, v0 = v1, v3 = v4 + +//! > ========================================================================== + +//! > Test reboxing through function call + +//! > TODO(eytan-starkware): Inter-procedural analysis to track equality through function calls + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(boxed: Box) -> Box { + let unboxed = boxed.unbox(); + let modified = identity(unboxed); + let reboxed = BoxTrait::new(modified); + reboxed +} + +//! > function_name +foo + +//! > module_code +#[inline(never)] +fn identity(x: felt252) -> felt252 { + x +} + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: core::felt252) <- unbox(v0) + (v2: core::felt252) <- test::identity(v1) + (v3: core::box::Box::) <- into_box(v2) +End: + Return(v3) + +//! > analysis_state +Block 0: +Box(v1) = v0, Box(v2) = v3 + +//! > ========================================================================== + +//! > Test reboxing through desnap - equality preserved + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(boxed: Box) -> (@Box, Box) { + let snap = @boxed; + let desnapped = *snap; + let unboxed = desnapped.unbox(); + let reboxed = BoxTrait::new(unboxed); + (snap, reboxed) +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: core::box::Box::, v2: @core::box::Box::) <- snapshot(v0) + (v3: core::felt252) <- unbox(v0) + (v4: core::box::Box::) <- into_box(v3) + (v5: (@core::box::Box::, core::box::Box::)) <- struct_construct(v2, v4) +End: + Return(v5) + +//! > analysis_state +Block 0: +@v0 = v2, Box(v3) = v0, v0 = v1, v0 = v4 + +//! > ========================================================================== + +//! > Test reboxing works through snapshot - equality tracked correctly + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(boxed: Box) -> Box { + let unboxed = boxed.unbox(); + let snap = @unboxed; + use_snap(snap); + let reboxed = BoxTrait::new(unboxed); + reboxed +} + +//! > function_name +foo + +//! > module_code +#[inline(never)] +fn use_snap(x: @felt252) {} + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: core::felt252) <- unbox(v0) + (v2: core::felt252, v3: @core::felt252) <- snapshot(v1) + () <- test::use_snap(v3) + (v4: core::box::Box::) <- into_box(v2) +End: + Return(v4) + +//! > analysis_state +Block 0: +@v1 = v3, Box(v1) = v0, v0 = v4, v1 = v2 + +//! > ========================================================================== + +//! > Test reboxing with different type - no equality + +//! > test_runner_name +test_equality_analysis + +//! > function_code +fn foo(boxed: Box) -> Box { + let unboxed: u32 = boxed.unbox(); + let converted: felt252 = unboxed.into(); + let reboxed = BoxTrait::new(converted); + reboxed +} + +//! > function_name +foo + +//! > module_code + +//! > semantic_diagnostics + +//! > lowering +Parameters: v0: core::box::Box:: +blk0 (root): +Statements: + (v1: core::integer::u32) <- unbox(v0) + (v2: core::felt252) <- core::integer::u32_to_felt252(v1) + (v3: core::box::Box::) <- into_box(v2) +End: + Return(v3) + +//! > analysis_state +Block 0: +Box(v1) = v0, Box(v2) = v3