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