Skip to main content

cairo_lang_lowering/analysis/
equality_analysis.rs

1//! Equality analysis for lowered IR.
2//!
3//! This module tracks semantic equivalence between variables as information flows through the
4//! program. Two variables are equivalent if they hold the same value. Additionally, the analysis
5//! tracks `Box`/unbox, snapshot/desnap, struct construct, and array construct relationships between
6//! equivalence classes via unified forward/reverse maps. Structs and arrays both map
7//! `(TypeId, Vec<FieldVar>)` but use distinct relations so they never hashcons together — array
8//! pop operations act as destructures on the array relation.
9
10use cairo_lang_debug::DebugWithDb;
11use cairo_lang_defs::ids::{ExternFunctionId, NamedLanguageElementId};
12use cairo_lang_semantic::corelib::option_some_variant;
13use cairo_lang_semantic::helper::ModuleHelper;
14use cairo_lang_semantic::{ConcreteVariant, GenericArgumentId, MatchArmSelector, TypeId};
15use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
16use salsa::Database;
17
18use crate::analysis::core::Edge;
19use crate::analysis::{DataflowAnalyzer, Direction, ForwardDataflowAnalysis};
20use crate::{
21    BlockEnd, BlockId, Lowered, MatchArm, MatchExternInfo, MatchInfo, Statement, VariableId,
22};
23
24/// A struct field variable: either a real variable or a unique placeholder for an unknown field.
25/// Placeholders are created during merge when a field has no intersection representative.
26#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
27enum FieldVar {
28    Var(VariableId),
29    /// A globally unique placeholder representing an unknown field.
30    Placeholder(usize),
31}
32
33impl FieldVar {
34    /// Returns the real variable if this is a `Var`, or `None` if it's a `Placeholder`.
35    fn as_var(self) -> Option<VariableId> {
36        match self {
37            FieldVar::Var(v) => Some(v),
38            FieldVar::Placeholder(_) => None,
39        }
40    }
41
42    /// Resolves the variable inside a `Var` to its representative, leaves `Placeholder` unchanged.
43    fn find_rep(self, info: &mut EqualityState<'_>) -> Self {
44        match self {
45            FieldVar::Var(v) => FieldVar::Var(info.find(v)),
46            p @ FieldVar::Placeholder(_) => p,
47        }
48    }
49}
50
51/// A relationship between equivalence classes, carrying its payload data.
52/// Hashable so it can be used as a forward map key.
53#[derive(Clone, Debug, Hash, PartialEq, Eq)]
54enum Relation<'db> {
55    Box(VariableId),
56    Snapshot(VariableId),
57    EnumConstruct(ConcreteVariant<'db>, VariableId),
58    StructConstruct(TypeId<'db>, Vec<FieldVar>),
59    Array(TypeId<'db>, Vec<FieldVar>),
60}
61
62impl<'db> Relation<'db> {
63    /// Returns an iterator over all real variables referenced by this relation.
64    fn referenced_vars(&self) -> impl Iterator<Item = VariableId> + '_ {
65        let (single, fields): (Option<VariableId>, &[FieldVar]) = match self {
66            Relation::Box(v) | Relation::Snapshot(v) | Relation::EnumConstruct(_, v) => {
67                (Some(*v), &[])
68            }
69            Relation::StructConstruct(_, vs) | Relation::Array(_, vs) => (None, vs),
70        };
71        single.into_iter().chain(fields.iter().filter_map(|f| f.as_var()))
72    }
73
74    /// Returns a new Relation with all input variables resolved to their current representatives.
75    fn with_fresh_reps(self, state: &mut EqualityState<'_>) -> Self {
76        match self {
77            Relation::Box(v) => Relation::Box(state.find(v)),
78            Relation::Snapshot(v) => Relation::Snapshot(state.find(v)),
79            Relation::EnumConstruct(variant, v) => Relation::EnumConstruct(variant, state.find(v)),
80            Relation::StructConstruct(ty, fields) => Relation::StructConstruct(
81                ty,
82                fields.into_iter().map(|f| f.find_rep(state)).collect(),
83            ),
84            Relation::Array(ty, fields) => {
85                Relation::Array(ty, fields.into_iter().map(|f| f.find_rep(state)).collect())
86            }
87        }
88    }
89}
90
91/// State for the equality analysis, tracking variable equivalences.
92///
93/// This is the `Info` type for the dataflow analysis. Each block gets its own
94/// `EqualityState` representing what we know at that point in the program.
95///
96/// Uses sparse HashMaps for efficiency - only variables that have been touched
97/// by the analysis are stored.
98#[derive(Clone, Debug, Default)]
99pub struct EqualityState<'db> {
100    /// Union-find parent map. If a variable is not in the map, it is its own representative.
101    union_find: OrderedHashMap<VariableId, VariableId>,
102
103    /// Forward map: Relation(...) -> output representative.
104    ///
105    /// Keys use representatives at insertion time. In SSA form, representatives are generally
106    /// stable within a block, so keys stay valid without migration during `union`. A union
107    /// *can* change a representative to a lower ID, which may cause a subsequent identical
108    /// construct to miss the earlier entry — this is a known imprecision (conservative, not
109    /// unsound). At merge points the maps are rebuilt from scratch.
110    forward: OrderedHashMap<Relation<'db>, VariableId>,
111
112    /// Reverse map: output representative -> Relation.
113    /// Records how each class was produced. A class has at most one reverse relationship.
114    reverse: OrderedHashMap<VariableId, Relation<'db>>,
115}
116
117impl<'db> EqualityState<'db> {
118    /// Gets the parent of a variable, defaulting to itself (root) if not in the map.
119    fn get_parent(&self, var: VariableId) -> VariableId {
120        self.union_find.get(&var).copied().unwrap_or(var)
121    }
122
123    /// Finds the representative of a variable's equivalence class.
124    /// Uses path splitting for efficiency: each node is redirected to its grandparent.
125    fn find(&mut self, mut var: VariableId) -> VariableId {
126        let mut parent = self.get_parent(var);
127        while parent != var {
128            let grandparent = self.get_parent(parent);
129            self.union_find.insert(var, grandparent);
130            var = parent;
131            parent = grandparent;
132        }
133        var
134    }
135
136    /// Finds the representative without modifying the structure.
137    pub(crate) fn find_immut(&self, mut var: VariableId) -> VariableId {
138        let mut parent = self.get_parent(var);
139        while parent != var {
140            var = parent;
141            parent = self.get_parent(var);
142        }
143        var
144    }
145
146    /// Unions the two variables into the same equivalence class.
147    /// `old_var`'s root is kept as the representative; `union_var`'s root joins it.
148    /// `union_var` must be a root, unless already in `old_var`'s class (no-op).
149    /// Returns the representative of the merged class.
150    fn add_equality(&mut self, old_var: VariableId, union_var: VariableId) -> VariableId {
151        let old_var = self.find(old_var);
152        let new_var = self.find(union_var);
153        if new_var == old_var {
154            return old_var;
155        }
156        assert!(union_var == new_var, "Expected variables to always be 'new' or equal to old");
157        self.union_find.insert(new_var, old_var);
158        old_var
159    }
160
161    /// Records a relation: forward maps `relation -> output`, reverse maps `output -> relation`.
162    /// If the same relation already maps to an existing output, unions them.
163    fn set_relation(&mut self, relation: Relation<'db>, output: VariableId) {
164        // Refresh reps — callers may pass stale IDs, and this maximizes forward hits.
165        let relation = relation.with_fresh_reps(self);
166
167        // Forward dedup: if this exact relation already maps to an output, union them.
168        let existing_output = if let Some(&existing_output) = self.forward.get(&relation) {
169            self.add_equality(existing_output, output)
170        } else {
171            output
172        };
173
174        // Insert with current reps (may be slightly stale after the union above).
175        self.forward.insert(relation.clone(), existing_output);
176        self.reverse.insert(existing_output, relation);
177    }
178
179    /// Looks up the struct construct info for a representative (immutable).
180    fn get_struct_construct_immut(&self, rep: VariableId) -> Option<(TypeId<'db>, Vec<FieldVar>)> {
181        match self.reverse.get(&rep)? {
182            Relation::StructConstruct(ty, fields) => Some((*ty, fields.clone())),
183            _ => None,
184        }
185    }
186
187    /// Looks up the struct construct info for a variable (mutable, uses find for path compression).
188    fn get_struct_construct(&mut self, var: VariableId) -> Option<(TypeId<'db>, Vec<FieldVar>)> {
189        let rep = self.find(var);
190        self.get_struct_construct_immut(rep)
191    }
192
193    /// Looks up the array construct info for a representative (immutable).
194    fn get_array_construct_immut(&self, rep: VariableId) -> Option<(TypeId<'db>, Vec<FieldVar>)> {
195        match self.reverse.get(&rep)? {
196            Relation::Array(ty, fields) => Some((*ty, fields.clone())),
197            _ => None,
198        }
199    }
200
201    /// Looks up the array construct info for a variable (mutable, uses find for path compression).
202    fn get_array_construct(&mut self, var: VariableId) -> Option<(TypeId<'db>, Vec<FieldVar>)> {
203        let rep = self.find(var);
204        self.get_array_construct_immut(rep)
205    }
206
207    /// Looks up the enum construct info for a variable (mutable, uses find for path compression).
208    pub(crate) fn get_enum_construct(
209        &self,
210        var: VariableId,
211    ) -> Option<(ConcreteVariant<'db>, VariableId)> {
212        let rep = self.find_immut(var);
213        self.get_enum_construct_immut(rep)
214    }
215
216    /// Looks up the enum construct info for a representative (immutable).
217    fn get_enum_construct_immut(
218        &self,
219        rep: VariableId,
220    ) -> Option<(ConcreteVariant<'db>, VariableId)> {
221        match self.reverse.get(&rep)? {
222            Relation::EnumConstruct(variant, input) => Some((*variant, *input)),
223            _ => None,
224        }
225    }
226}
227
228impl<'db> DebugWithDb<'db> for EqualityState<'db> {
229    type Db = dyn Database;
230
231    fn fmt(&self, f: &mut std::fmt::Formatter<'_>, db: &'db Self::Db) -> std::fmt::Result {
232        let v = |id: VariableId| format!("v{}", self.find_immut(id).index());
233        let mut lines = Vec::<String>::new();
234        for (relation, &output) in self.forward.iter() {
235            match relation {
236                Relation::Snapshot(source) => {
237                    lines.push(format!("@{} = {}", v(*source), v(output)));
238                }
239                Relation::Box(source) => {
240                    lines.push(format!("Box({}) = {}", v(*source), v(output)));
241                }
242                Relation::EnumConstruct(variant, input) => {
243                    let name = variant.id.name(db).to_string(db);
244                    lines.push(format!("{name}({}) = {}", v(*input), v(output)));
245                }
246                Relation::StructConstruct(ty, inputs) | Relation::Array(ty, inputs) => {
247                    let type_name = ty.format(db);
248                    let fields = inputs
249                        .iter()
250                        .map(|f| match f {
251                            FieldVar::Var(id) => v(*id),
252                            FieldVar::Placeholder(_) => "?".to_string(),
253                        })
254                        .collect::<Vec<_>>()
255                        .join(", ");
256                    // Structs render as `Type(..)`, arrays as `Type[..]`, to disambiguate the two
257                    // relations that share a `(TypeId, Vec<FieldVar>)` payload.
258                    let (open, close) = match relation {
259                        Relation::Array(..) => ('[', ']'),
260                        _ => ('(', ')'),
261                    };
262                    lines.push(format!("{type_name}{open}{fields}{close} = {}", v(output)));
263                }
264            }
265        }
266        for &var in self.union_find.keys() {
267            let rep = self.find_immut(var);
268            if var != rep {
269                lines.push(format!("v{} = v{}", rep.index(), var.index()));
270            }
271        }
272        lines.sort();
273        if lines.is_empty() { write!(f, "(empty)") } else { write!(f, "{}", lines.join(", ")) }
274    }
275}
276
277/// Variable equality analysis.
278///
279/// This analyzer tracks snapshot/desnap, box/unbox, struct construct, and array construct
280/// relationships as data
281/// flows through the program. At merge points (after match arms converge), we union-find variables
282/// that share the same equivalence classes in **both** branches (`(rep1, rep2)` meet), rebuild
283/// `forward` / `reverse`, and intersect relations **without** mapping variables across differing
284/// branch-local equivalence roots (structure fields fall back to placeholders).
285pub struct EqualityAnalysis<'a, 'db> {
286    db: &'db dyn Database,
287    lowered: &'a Lowered<'db>,
288    /// Counter for allocating globally unique `Placeholder` IDs for unknown struct/array fields
289    /// after merge.
290    next_placeholder: usize,
291    /// The `array_new` extern function id.
292    array_new: ExternFunctionId<'db>,
293    /// The `array_append` extern function id.
294    array_append: ExternFunctionId<'db>,
295    /// The `array_pop_front` extern function id.
296    array_pop_front: ExternFunctionId<'db>,
297    /// The `array_pop_front_consume` extern function id.
298    array_pop_front_consume: ExternFunctionId<'db>,
299    /// The `array_snapshot_pop_front` extern function id.
300    array_snapshot_pop_front: ExternFunctionId<'db>,
301    /// The `array_snapshot_pop_back` extern function id.
302    array_snapshot_pop_back: ExternFunctionId<'db>,
303}
304
305impl<'a, 'db> EqualityAnalysis<'a, 'db> {
306    /// Creates a new equality analysis instance.
307    pub fn new(db: &'db dyn Database, lowered: &'a Lowered<'db>) -> Self {
308        let array_module = ModuleHelper::core(db).submodule("array");
309        Self {
310            db,
311            lowered,
312            next_placeholder: 0,
313            array_new: array_module.extern_function_id("array_new"),
314            array_append: array_module.extern_function_id("array_append"),
315            array_pop_front: array_module.extern_function_id("array_pop_front"),
316            array_pop_front_consume: array_module.extern_function_id("array_pop_front_consume"),
317            array_snapshot_pop_front: array_module.extern_function_id("array_snapshot_pop_front"),
318            array_snapshot_pop_back: array_module.extern_function_id("array_snapshot_pop_back"),
319        }
320    }
321
322    /// Runs equality analysis on a lowered function.
323    /// Returns the equality state at the exit of each block.
324    pub fn analyze(
325        db: &'db dyn Database,
326        lowered: &'a Lowered<'db>,
327    ) -> Vec<Option<EqualityState<'db>>> {
328        ForwardDataflowAnalysis::new(lowered, EqualityAnalysis::new(db, lowered)).run()
329    }
330
331    /// Handles extern match arms for array operations.
332    ///
333    /// Array pop operations act as "destructures" on the array construct representation:
334    /// - `array_pop_front` / `array_pop_front_consume`: On the Some arm, if the input array was
335    ///   tracked as `[e0, e1, ..., eN]`, the popped element (boxed) is `Box(e0)` and the remaining
336    ///   array is `[e1, ..., eN]`.
337    /// - `array_snapshot_pop_front`: Same as above but through snapshot/box-of-snapshot wrappers.
338    /// - `array_snapshot_pop_back`: Like pop_front but pops from the back: element is `Box(eN)`,
339    ///   remaining is `[e0, ..., eN-1]`.
340    ///
341    /// The `None` arms are intentionally ignored: recording empty-array relations or unions there
342    /// would backedge-merge older SSA equivalence classes based on branch discrimination.
343    fn transfer_extern_match_arm(
344        &self,
345        info: &mut EqualityState<'db>,
346        extern_info: &MatchExternInfo<'db>,
347        arm: &MatchArm<'db>,
348    ) {
349        let Some((id, _)) = extern_info.function.get_extern(self.db) else { return };
350        // TODO(eytan-starkware): Add support for multipop.
351        let MatchArmSelector::VariantId(variant) = arm.arm_selector else { return };
352        if id == self.array_pop_front
353            || id == self.array_pop_front_consume
354            || id == self.array_snapshot_pop_front
355            || id == self.array_snapshot_pop_back
356        {
357            let [GenericArgumentId::Type(option_ty)] =
358                variant.concrete_enum_id.long(self.db).generic_args[..]
359            else {
360                panic!("Expected Option<T> with a single type argument");
361            };
362            let some_variant = option_some_variant(self.db, option_ty);
363            assert_eq!(
364                variant.concrete_enum_id.enum_id(self.db),
365                some_variant.concrete_enum_id.enum_id(self.db),
366                "Expected match to be on an Option<T>"
367            );
368            self.transfer_array_pop_arm(info, extern_info, arm, id, variant == some_variant);
369        }
370    }
371
372    /// Handles the actual array pop arm transfer after validating the Option variant.
373    fn transfer_array_pop_arm(
374        &self,
375        info: &mut EqualityState<'db>,
376        extern_info: &MatchExternInfo<'db>,
377        arm: &MatchArm<'db>,
378        id: ExternFunctionId<'db>,
379        is_some: bool,
380    ) {
381        if (id == self.array_pop_front || id == self.array_pop_front_consume) && is_some {
382            // Some arm: var_ids = [remaining_arr, boxed_elem]
383            let input_arr = extern_info.inputs[0].var_id;
384            let remaining_arr = arm.var_ids[0];
385            let boxed_elem = arm.var_ids[1];
386            if let Some((ty, elems)) = info.get_array_construct(input_arr)
387                && let Some((first, rest)) = elems.split_first()
388            {
389                // TODO(eytan-starkware): Add support for placeholders in boxes and reverse box
390                // relation to unify placeholder.
391                if let FieldVar::Var(first_var) = first {
392                    info.set_relation(Relation::Box(*first_var), boxed_elem);
393                }
394                let rest_fields: Vec<FieldVar> = rest.iter().map(|f| f.find_rep(info)).collect();
395                info.set_relation(Relation::Array(ty, rest_fields), remaining_arr);
396            }
397        } else if (id == self.array_snapshot_pop_front || id == self.array_snapshot_pop_back)
398            && is_some
399        {
400            // Some arm: var_ids = [remaining_snap_arr, boxed_snap_elem]
401            let input_snap_arr = extern_info.inputs[0].var_id;
402            let remaining_snap_arr = arm.var_ids[0];
403            let boxed_snap_elem = arm.var_ids[1];
404
405            // Look up tracked elements via snapshot reverse relationship or direct lookup.
406            let snap_rep = info.find(input_snap_arr);
407            let original_rep = match info.reverse.get(&snap_rep) {
408                Some(Relation::Snapshot(v)) => Some(*v),
409                _ => None,
410            };
411            let elems_opt = original_rep
412                .and_then(|orig| {
413                    let orig = info.find_immut(orig);
414                    info.get_array_construct_immut(orig)
415                })
416                .or_else(|| info.get_array_construct_immut(snap_rep));
417
418            let snap_ty = self.lowered.variables[remaining_snap_arr].ty;
419            let Some((_orig_ty, elems)) = elems_opt else {
420                return;
421            };
422            let pop_front = id == self.array_snapshot_pop_front;
423            let (elem, rest) = if pop_front {
424                let Some((first, tail)) = elems.split_first() else {
425                    info.set_relation(Relation::Array(snap_ty, vec![]), remaining_snap_arr);
426                    return;
427                };
428                (*first, tail)
429            } else {
430                let Some((last, init)) = elems.split_last() else {
431                    info.set_relation(Relation::Array(snap_ty, vec![]), remaining_snap_arr);
432                    return;
433                };
434                (*last, init)
435            };
436
437            // The popped element is `Box<@T>`. Record the box relationship against
438            // the snapshot class of `elem` if it exists.
439            // TODO(eytan-starkware): Support relationships even when no variable
440            // represents `@elem` yet (elem is a Placeholder).
441            if let FieldVar::Var(elem_var) = elem {
442                let elem_rep = info.find(elem_var);
443                if let Some(&snap_of_elem) = info.forward.get(&Relation::Snapshot(elem_rep)) {
444                    info.set_relation(Relation::Box(snap_of_elem), boxed_snap_elem);
445                }
446            }
447
448            let rest_fields: Vec<FieldVar> = rest.iter().map(|f| f.find_rep(info)).collect();
449            info.set_relation(Relation::Array(snap_ty, rest_fields), remaining_snap_arr);
450        }
451    }
452}
453
454/// Returns an iterator over all variables with equality or relationship information in the equality
455/// states.
456fn merge_referenced_vars<'db, 'a>(
457    info1: &'a EqualityState<'db>,
458    info2: &'a EqualityState<'db>,
459) -> impl Iterator<Item = VariableId> + 'a {
460    let union_find_vars = info1.union_find.keys().chain(info2.union_find.keys()).copied();
461
462    let forward_vars =
463        info1.forward.iter().chain(info2.forward.iter()).flat_map(|(relation, &output)| {
464            relation.referenced_vars().chain(std::iter::once(output))
465        });
466
467    let reverse_vars = info1
468        .reverse
469        .iter()
470        .chain(info2.reverse.iter())
471        .flat_map(|(&rep, relation)| std::iter::once(rep).chain(relation.referenced_vars()));
472
473    union_find_vars.chain(forward_vars).chain(reverse_vars)
474}
475
476/// Keeps relational edges that survive the join **without** mapping variables across mismatched
477/// branch equivalence roots: struct/array fields become placeholders unless both sides cite the
478/// same SSA `VariableId`. Box/Snapshot/Enum reuse an edge only when merged outputs coincide in
479/// `result`.
480fn merge_relations<'db>(
481    info1: &EqualityState<'db>,
482    info2: &EqualityState<'db>,
483    intersections: &OrderedHashMap<VariableId, Vec<(VariableId, VariableId)>>,
484    result: &mut EqualityState<'db>,
485    next_placeholder: &mut usize,
486) {
487    // Index info2's aggregate (struct/array) entries by output representative for lookup in the
488    // merge loop. Forward entries are completely authoritative, so we use them to build the index.
489    // Structs and arrays are indexed separately so they never cross-match during the intersection.
490    let mut info2_structs_by_output: OrderedHashMap<VariableId, (TypeId<'db>, Vec<FieldVar>)> =
491        OrderedHashMap::default();
492    let mut info2_arrays_by_output: OrderedHashMap<VariableId, (TypeId<'db>, Vec<FieldVar>)> =
493        OrderedHashMap::default();
494    for (relation, &output2) in info2.forward.iter() {
495        match relation {
496            Relation::StructConstruct(ty, fields) => {
497                info2_structs_by_output.insert(info2.find_immut(output2), (*ty, fields.clone()));
498            }
499            Relation::Array(ty, fields) => {
500                info2_arrays_by_output.insert(info2.find_immut(output2), (*ty, fields.clone()));
501            }
502            _ => {}
503        }
504    }
505
506    // Iterate all forward entries from info1 and find matching entries in info2.
507    for (relation, &output1) in info1.forward.iter() {
508        match relation {
509            Relation::Box(source1) | Relation::Snapshot(source1) => {
510                for &(source2, intersection_var) in intersections.get(source1).into_iter().flatten()
511                {
512                    let relation2 = match relation {
513                        Relation::Box(_) => Relation::Box(source2),
514                        Relation::Snapshot(_) => Relation::Snapshot(source2),
515                        _ => unreachable!(),
516                    };
517                    let Some(&output2) = info2.forward.get(&relation2) else { continue };
518                    // Require the same unified output (join meet); avoid mapping via rep pair
519                    // lookup.
520                    // TODO(eytan-starkware): We want to check that the intersection is not empty
521                    // and use that for the output rep, not that the leaders are there.
522                    let output_rep = result.find(output1);
523                    if output_rep != result.find_immut(output2) {
524                        continue;
525                    }
526                    let result_relation = match relation {
527                        Relation::Box(_) => Relation::Box(result.find(intersection_var)),
528                        Relation::Snapshot(_) => Relation::Snapshot(result.find(intersection_var)),
529                        _ => unreachable!(),
530                    };
531                    result.set_relation(result_relation, output_rep);
532                }
533            }
534            Relation::EnumConstruct(variant, input1) => {
535                for &(input2, input_intersection) in
536                    intersections.get(&info1.find_immut(*input1)).into_iter().flatten()
537                {
538                    let relation2 = Relation::EnumConstruct(*variant, input2);
539                    let Some(&output2) = info2.forward.get(&relation2) else { continue };
540                    let output_rep = result.find(output1);
541                    if output_rep != result.find_immut(output2) {
542                        continue;
543                    }
544                    result.set_relation(
545                        Relation::EnumConstruct(*variant, input_intersection),
546                        output_rep,
547                    );
548                }
549            }
550            Relation::StructConstruct(ty, fields1) | Relation::Array(ty, fields1) => {
551                let is_array = matches!(relation, Relation::Array(..));
552                let index =
553                    if is_array { &info2_arrays_by_output } else { &info2_structs_by_output };
554                let output1_rep = info1.find_immut(output1);
555                for &(output2_rep, output_intersection) in
556                    intersections.get(&output1_rep).into_iter().flatten()
557                {
558                    let Some((ty2, fields2)) = index.get(&output2_rep) else {
559                        continue;
560                    };
561                    if ty2 != ty || fields2.len() != fields1.len() {
562                        continue;
563                    }
564                    let mut any_data = false;
565                    let result_fields: Vec<FieldVar> = fields1
566                        .iter()
567                        .zip(fields2)
568                        .map(|(f1, f2)| match (f1, f2) {
569                            (FieldVar::Var(v), FieldVar::Var(w))
570                                if result.find(*v) == result.find(*w) =>
571                            {
572                                any_data = true;
573                                FieldVar::Var(result.find(*v))
574                            }
575                            (_, _) => {
576                                *next_placeholder += 1;
577                                FieldVar::Placeholder(*next_placeholder - 1)
578                            }
579                        })
580                        .collect();
581                    if any_data || result_fields.is_empty() {
582                        let result_relation = if is_array {
583                            Relation::Array(*ty, result_fields)
584                        } else {
585                            Relation::StructConstruct(*ty, result_fields)
586                        };
587                        result.set_relation(result_relation, output_intersection);
588                    }
589                }
590            }
591        }
592    }
593}
594
595impl<'db, 'a> DataflowAnalyzer<'db, 'a> for EqualityAnalysis<'a, 'db> {
596    type Info = EqualityState<'db>;
597
598    const DIRECTION: Direction = Direction::Forward;
599
600    fn initial_info(&mut self, _block_id: BlockId, _block_end: &'a BlockEnd<'db>) -> Self::Info {
601        EqualityState::default()
602    }
603
604    fn merge(
605        &mut self,
606        _lowered: &Lowered<'db>,
607        _statement_location: super::StatementLocation,
608        info1: Self::Info,
609        info2: Self::Info,
610    ) -> Self::Info {
611        // Meet of union-finds, then intersect relations (`merge_relations`) without bridging
612        // mismatched equiv-class reps across branches (struct fields → placeholders unless same SSA
613        // id).
614        let mut result = EqualityState::default();
615
616        // Group variables by (rep1, rep2) - for variables present in either state.
617        let mut groups: OrderedHashMap<(VariableId, VariableId), Vec<VariableId>> =
618            OrderedHashMap::default();
619
620        // Group by (rep1, rep2). Duplicates are fine - they'll just be added to the same group.
621        for var in merge_referenced_vars(&info1, &info2) {
622            let key = (info1.find_immut(var), info2.find_immut(var));
623            groups.entry(key).or_default().push(var);
624        }
625
626        // Union all variables within each group. Members are fresh in `result`
627        // (which was just constructed), so any choice of `keep` is flat-preserving;
628        // the lowest-ID is chosen so the rep is stable/deterministic.
629        for members in groups.values() {
630            let Some(&keep) = members.iter().min_by_key(|v| v.index()) else { continue };
631            for &var in members {
632                if var != keep {
633                    result.add_equality(keep, var);
634                }
635            }
636        }
637
638        // Secondary index: rep₁ -> `(rep₂, representative in result)` for relation intersection.
639        let mut intersections: OrderedHashMap<VariableId, Vec<(VariableId, VariableId)>> =
640            OrderedHashMap::default();
641        for (&(rep1, rep2), vars) in groups.iter() {
642            intersections.entry(rep1).or_default().push((rep2, result.find(vars[0])));
643        }
644
645        merge_relations(&info1, &info2, &intersections, &mut result, &mut self.next_placeholder);
646
647        result
648    }
649
650    fn transfer_stmt(
651        &mut self,
652        info: &mut Self::Info,
653        _statement_location: super::StatementLocation,
654        stmt: &'a Statement<'db>,
655    ) {
656        match stmt {
657            Statement::Snapshot(snapshot_stmt) => {
658                info.add_equality(snapshot_stmt.input.var_id, snapshot_stmt.original());
659                info.set_relation(
660                    Relation::Snapshot(snapshot_stmt.input.var_id),
661                    snapshot_stmt.snapshot(),
662                );
663            }
664
665            Statement::Desnap(desnap_stmt) => {
666                let input_rep = info.find(desnap_stmt.input.var_id);
667                if let Some(Relation::Snapshot(old_inner)) = info.reverse.get(&input_rep) {
668                    info.add_equality(*old_inner, desnap_stmt.output);
669                } else {
670                    info.set_relation(
671                        Relation::Snapshot(desnap_stmt.output),
672                        desnap_stmt.input.var_id,
673                    );
674                }
675            }
676
677            Statement::IntoBox(into_box_stmt) => {
678                info.set_relation(Relation::Box(into_box_stmt.input.var_id), into_box_stmt.output);
679            }
680
681            Statement::Unbox(unbox_stmt) => {
682                let input_rep = info.find(unbox_stmt.input.var_id);
683                if let Some(Relation::Box(old_inner)) = info.reverse.get(&input_rep) {
684                    info.add_equality(*old_inner, unbox_stmt.output);
685                } else {
686                    info.set_relation(Relation::Box(unbox_stmt.output), unbox_stmt.input.var_id);
687                }
688            }
689
690            Statement::EnumConstruct(enum_stmt) => {
691                // output = Variant(input): track via forward map
692                // If we've already seen this variant with an equivalent input, the outputs are
693                // equal.
694                info.set_relation(
695                    Relation::EnumConstruct(enum_stmt.variant, enum_stmt.input.var_id),
696                    enum_stmt.output,
697                );
698            }
699
700            Statement::StructConstruct(struct_stmt) => {
701                // output = StructType(inputs...): track via forward map
702                // If we've already seen the same struct type with equivalent inputs, the outputs
703                // are equal.
704                let ty = self.lowered.variables[struct_stmt.output].ty;
705                let fields =
706                    struct_stmt.inputs.iter().map(|i| FieldVar::Var(info.find(i.var_id))).collect();
707                info.set_relation(Relation::StructConstruct(ty, fields), struct_stmt.output);
708            }
709
710            Statement::StructDestructure(struct_stmt) => {
711                // (outputs...) = struct_destructure(input)
712                let struct_var = info.find(struct_stmt.input.var_id);
713                // 1. If input was previously constructed, union outputs with original fields. Skip
714                //    placeholder fields (unknown after merge).
715                if let Some((_, field_reps)) = info.get_struct_construct(struct_var) {
716                    for (&output, field) in struct_stmt.outputs.iter().zip(field_reps.iter()) {
717                        if let FieldVar::Var(field_rep) = field {
718                            info.add_equality(*field_rep, output);
719                        }
720                    }
721                }
722                // 2. Record: struct_construct(outputs) == input (for future constructs).
723                let ty = self.lowered.variables[struct_var].ty;
724                let fields =
725                    struct_stmt.outputs.iter().map(|&v| FieldVar::Var(info.find(v))).collect();
726                info.set_relation(Relation::StructConstruct(ty, fields), struct_var);
727            }
728
729            Statement::Call(call_stmt) => {
730                let Some((id, _)) = call_stmt.function.get_extern(self.db) else { return };
731                if id == self.array_new {
732                    let ty = self.lowered.variables[call_stmt.outputs[0]].ty;
733                    info.set_relation(Relation::Array(ty, vec![]), call_stmt.outputs[0]);
734                } else if id == self.array_append
735                    && let Some((ty, elems)) = info.get_array_construct(call_stmt.inputs[0].var_id)
736                {
737                    // Only track append if the input array is already tracked. Arrays from
738                    // function parameters or external calls are conservatively ignored.
739                    let mut new_elems = elems;
740                    new_elems.push(FieldVar::Var(info.find(call_stmt.inputs[1].var_id)));
741                    info.set_relation(Relation::Array(ty, new_elems), call_stmt.outputs[0]);
742                }
743            }
744
745            Statement::Const(_) => {}
746        }
747    }
748
749    fn transfer_edge(&mut self, info: &Self::Info, edge: &Edge<'db, 'a>) -> Self::Info {
750        let mut new_info = info.clone();
751        match edge {
752            Edge::Goto { remapping, .. } => {
753                // Union remapped variables: dst and src should be in the same equivalence class
754                for (dst, src_usage) in remapping.iter() {
755                    new_info.add_equality(src_usage.var_id, *dst);
756                }
757            }
758            Edge::MatchArm { arm, match_info } => {
759                // For enum matches, track that matched_var = Variant(arm_var).
760                if let MatchInfo::Enum(enum_info) = match_info
761                    && let MatchArmSelector::VariantId(variant) = arm.arm_selector
762                    && let [arm_var] = arm.var_ids[..]
763                {
764                    let matched_var = enum_info.input.var_id;
765
766                    // If we previously saw this enum constructed with the same variant,
767                    // union with the original input. Skip if variants differ — this can
768                    // happen after optimizations merge states from different branches.
769                    let matched_rep = new_info.find(matched_var);
770                    if let Some((old_variant, input)) =
771                        new_info.get_enum_construct_immut(matched_rep)
772                        && variant == old_variant
773                    {
774                        new_info.add_equality(input, arm_var);
775                    }
776
777                    // Record the relationship: matched_var = Variant(arm_var)
778                    new_info.set_relation(Relation::EnumConstruct(variant, arm_var), matched_var);
779                }
780
781                // For extern matches on array operations, track pop/destructure relationships.
782                if let MatchInfo::Extern(extern_info) = match_info {
783                    self.transfer_extern_match_arm(&mut new_info, extern_info, arm);
784                }
785            }
786            Edge::Return { .. } | Edge::Panic { .. } => {}
787        }
788        new_info
789    }
790}