Skip to main content

cairo_lang_lowering/optimizations/
variable_forwarding.rs

1#[cfg(test)]
2#[path = "variable_forwarding_test.rs"]
3mod test;
4
5use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
6use cairo_lang_utils::ordered_hash_set::OrderedHashSet;
7use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
8use cairo_lang_utils::unordered_hash_set::UnorderedHashSet;
9use itertools::Itertools;
10use salsa::Database;
11
12use crate::analysis::def_site::DefSiteAnalysis;
13use crate::analysis::dominator::Dominators;
14use crate::analysis::equality_analysis::{EqualityAnalysis, EqualityState};
15use crate::analysis::topological_order::TopologicalOrder;
16use crate::analysis::use_sites::UseSites;
17use crate::analysis::{DefLocation, UseLocation};
18use crate::objects::blocks::Blocks;
19use crate::{BlockEnd, BlockId, Lowered, Statement, VariableArena, VariableId};
20
21/// A rename to apply when a definition's removal is finalized.
22struct RenameDep {
23    loc: UseLocation,
24    from: VariableId,
25    to: VariableId,
26    /// Multiplicity of `from` at `loc`: how many consuming slots this rename moves onto
27    /// `to`. Kept so freed/added counts stay in the same per-slot unit as `use_count`.
28    count: usize,
29}
30
31/// State committed from successful removal transactions.
32#[derive(Default)]
33struct CommittedState {
34    /// Removed statements, grouped by block for efficient Phase 3 application.
35    removed: OrderedHashMap<BlockId, UnorderedHashSet<usize>>,
36    /// Renames to apply at use locations; gated by `removed` so renames at removed stmts skip.
37    renames: Vec<RenameDep>,
38    /// Per-variable count of uses freed by committed removals.
39    freed_count: UnorderedHashMap<VariableId, usize>,
40    /// Per-variable count of uses added by committed renames.
41    added_count: UnorderedHashMap<VariableId, usize>,
42}
43
44impl CommittedState {
45    fn is_stmt_removed(&self, block_id: BlockId, stmt_idx: usize) -> bool {
46        self.removed.get(&block_id).is_some_and(|indices| indices.contains(&stmt_idx))
47    }
48
49    fn merge(&mut self, txn: Transaction) {
50        for def in txn.removed {
51            // TODO: We should deal with all def locations
52            if let DefLocation::Statement((block_id, stmt_idx)) = def {
53                self.removed.entry(block_id).or_default().insert(stmt_idx);
54            }
55        }
56        self.renames.extend(txn.renames);
57        for (v, delta) in txn.freed_delta {
58            *self.freed_count.entry(v).or_default() += delta;
59        }
60        for (v, delta) in txn.added_delta {
61            *self.added_count.entry(v).or_default() += delta;
62        }
63    }
64}
65
66/// A transaction — local delta on top of committed state.
67/// Merged into committed on success, discarded on failure (no effect).
68#[derive(Default)]
69struct Transaction {
70    /// Definitions tentatively marked for removal in this transaction.
71    removed: OrderedHashSet<DefLocation>,
72    /// Renames tentatively recorded; reach committed state on merge.
73    renames: Vec<RenameDep>,
74    /// Per-variable count of uses freed within this transaction.
75    freed_delta: OrderedHashMap<VariableId, usize>,
76    /// Per-variable count of uses added by tentative renames within this transaction.
77    added_delta: OrderedHashMap<VariableId, usize>,
78}
79
80/// Applies variable forwarding optimization.
81///
82/// Each def is attempted at most once, in backward (reverse-topological) order. Each attempt
83/// is an isolated transaction that recursively removes producers whose outputs become
84/// unconsumed, committing on success. Failed attempts are never retried — the order and
85/// no-retry are therefore load-bearing for correctness.
86pub fn variable_forwarding<'db>(db: &'db dyn Database, lowered: &mut Lowered<'db>) {
87    if lowered.blocks.is_empty() {
88        return;
89    }
90
91    let equalities = EqualityAnalysis::analyze(db, lowered);
92    let dominators = Dominators::analyze(lowered);
93    let def_sites = DefSiteAnalysis::analyze(lowered).0;
94    let use_sites = UseSites::analyze(lowered);
95    let topological_order = TopologicalOrder::analyze(lowered);
96    let ctx = ForwardingCtx {
97        variables: &lowered.variables,
98        blocks: &mut lowered.blocks,
99        dominators,
100        def_sites,
101        use_sites,
102        equalities: &equalities,
103    };
104    ctx.forward_all(topological_order);
105}
106
107/// Working context for the forwarding optimization.
108struct ForwardingCtx<'a, 'db> {
109    /// Variable arena (for copyable/droppable checks).
110    variables: &'a VariableArena<'db>,
111    /// Mutable blocks; used for both reading statement inputs/outputs and applying renames.
112    blocks: &'a mut Blocks<'db>,
113    /// Block dominator tree, for visibility checks across blocks.
114    dominators: Dominators,
115    /// Definition site for each variable, indexed by variable arena index.
116    def_sites: Vec<Option<DefLocation>>,
117    /// Use sites for each variable, mutated in-place by `prefill` renames.
118    use_sites: UseSites,
119    /// Per-block equality analysis state, used to find each variable's representative.
120    equalities: &'a [Option<EqualityState<'db>>],
121}
122
123impl<'a, 'db> ForwardingCtx<'a, 'db> {
124    fn forward_all(mut self, topological_order: TopologicalOrder) {
125        // Phase 1: Forward copy+droppable types eagerly.
126        self.prefill();
127
128        // Phase 2: attempt each def in backward (reverse-topological) order. This is
129        // load-bearing: a representative is always an earlier-defined variable, so visiting
130        // later defs first records every rename onto a rep before its producer is tried, where
131        // `has_added_uses` blocks removing a still-consumed producer (otherwise a dangling
132        // "rename-then-remove"). Relies on this order plus at-most-once, never-retried attempts.
133        let mut committed = CommittedState::default();
134        let mut tried: UnorderedHashSet<DefLocation> = UnorderedHashSet::default();
135        for block_id in topological_order.iter().rev() {
136            for stmt_idx in (0..self.blocks[*block_id].statements.len()).rev() {
137                if self.blocks[*block_id].statements[stmt_idx].outputs().is_empty() {
138                    continue;
139                }
140                let mut txn = Transaction::default();
141                if self.try_remove(
142                    DefLocation::Statement((*block_id, stmt_idx)),
143                    &mut tried,
144                    &committed,
145                    &mut txn,
146                ) && self.verify_non_copy_safety(&committed, &txn)
147                {
148                    committed.merge(txn);
149                }
150            }
151        }
152
153        // Phase 3: Apply committed renames and remove statements.
154        for r in &committed.renames {
155            if let UseLocation::Statement((block_id, stmt_idx)) = r.loc
156                && committed.is_stmt_removed(block_id, stmt_idx)
157            {
158                continue;
159            }
160            self.rename_var(r.loc, r.from, r.to);
161        }
162        for (block_id, indices) in committed.removed.iter() {
163            let mut i = 0;
164            self.blocks[*block_id].statements.retain(|_| {
165                let keep = !indices.contains(&i);
166                i += 1;
167                keep
168            });
169        }
170    }
171
172    /// Forwards copy+droppable variables to their representatives.
173    fn prefill(&mut self) {
174        for (var_id, var) in self.variables {
175            let Some(def) = self.definition(var_id) else {
176                continue;
177            };
178            if !self.removable(def) || var.info.copyable.is_err() || var.info.droppable.is_err() {
179                continue;
180            }
181            // Collect to break borrow on self (rename_var / use_sites mutate).
182            for (loc, _count) in self.use_sites.use_locs(var_id).collect_vec() {
183                let Some(eq_state) = self.equalities[loc.block().0].as_ref() else {
184                    continue;
185                };
186                let lead = eq_state.find_immut(var_id);
187                if lead != var_id && self.is_visible(loc, lead) {
188                    self.rename_var(loc, var_id, lead);
189                    // rename_var rewrote every slot at `loc`; move the whole count too.
190                    self.use_sites.move_uses(var_id, lead, loc);
191                }
192            }
193        }
194    }
195
196    /// Tries to remove `def` within the current transaction. Recursively resolves
197    /// non-drop obligations by removing producers whose outputs become unconsumed.
198    /// Returns true if the removal (and all cascaded obligations) succeeded.
199    /// On failure, `txn` may contain partial changes — caller should discard the txn.
200    fn try_remove(
201        &self,
202        def: DefLocation,
203        tried: &mut UnorderedHashSet<DefLocation>,
204        committed: &CommittedState,
205        txn: &mut Transaction,
206    ) -> bool {
207        if self.is_removed(def, committed, txn) {
208            return true;
209        }
210        // Linearization: try each def at most once globally.
211        if !tried.insert(def) {
212            return false;
213        }
214
215        let Some(deps) = self.try_forward_all_outputs(def, committed, txn) else {
216            return false;
217        };
218
219        // Record removal within transaction.
220        txn.removed.insert(def);
221        for r in &deps {
222            *txn.added_delta.entry(r.to).or_default() += r.count;
223        }
224        txn.renames.extend(deps);
225
226        // Free inputs and resolve non-copy obligations.
227        let DefLocation::Statement(stmt_loc) = def else { return true };
228        // Free one use per input slot, matching `UseSites`' per-slot multiplicity
229        // counts: a statement consuming the same variable in two slots frees two of its
230        // uses, which is exactly what `use_count` recorded for that location.
231        for var_usage in self.blocks[stmt_loc].inputs() {
232            let var_id = var_usage.var_id;
233            *txn.freed_delta.entry(var_id).or_default() += 1;
234
235            if self.variables[var_id].info.copyable.is_err()
236                && self.effective_use_count(var_id, committed, txn) == 0
237            {
238                // Non-copy variable with no remaining consumers — try to remove
239                // the producer so that renames targeting `v` don't create duplicate
240                // consuming uses. If the cascade fails, propagate failure so the
241                // whole transaction is discarded; partial cascade state is left in
242                // `txn` and will be dropped together with the rest by the worklist
243                // when this top-level `try_remove` returns false.
244                let Some(producer) = self.definition(var_id) else {
245                    continue;
246                };
247                if !self.try_remove(producer, tried, committed, txn) {
248                    return false;
249                }
250            }
251        }
252        true
253    }
254
255    /// Returns the renames needed to forward all uses of `v` to its representative,
256    /// or `None` if any use cannot be forwarded.
257    /// Non-consuming uses (snapshots) are included — they need renaming too.
258    fn forwarding_dependencies(
259        &self,
260        v: VariableId,
261        committed: &CommittedState,
262        txn: &Transaction,
263    ) -> Option<Vec<RenameDep>> {
264        let mut deps = Vec::new();
265        for (loc, count) in self.use_sites.use_locs(v) {
266            if let UseLocation::Statement(sl) = loc
267                && self.is_removed(DefLocation::Statement(sl), committed, txn)
268            {
269                continue;
270            }
271            let eq_state = self.equalities[loc.block().0].as_ref()?;
272            // Snapshots are NOT rejected — they generate renames like any other use.
273            let rep = eq_state.find_immut(v);
274            if rep == v || !self.is_visible(loc, rep) {
275                return None;
276            }
277            deps.push(RenameDep { loc, from: v, to: rep, count });
278        }
279        // Fail if v gained uses from renames. These added uses aren't tracked in
280        // use_sites, so the renames above wouldn't cover them — removing v's
281        // producer would leave dangling references.
282        if self.has_added_uses(v, committed, txn) {
283            return None;
284        }
285        Some(deps)
286    }
287
288    /// Checks ALL outputs of a node. Returns combined rename list or `None`.
289    fn try_forward_all_outputs(
290        &self,
291        def: DefLocation,
292        committed: &CommittedState,
293        txn: &Transaction,
294    ) -> Option<Vec<RenameDep>> {
295        if !self.removable(def) {
296            return None;
297        }
298        // TODO(eytan-starkware): Handle BlockEntry from goto remappings.
299        let DefLocation::Statement(sl) = def else { return None };
300        let outputs: Vec<VariableId> = self.blocks[sl].outputs().to_vec();
301        let mut all_renames = Vec::new();
302        for v in outputs {
303            all_renames.extend(self.forwarding_dependencies(v, committed, txn)?);
304        }
305        Some(all_renames)
306    }
307
308    /// Effective use count: original minus freed plus added.
309    fn effective_use_count(
310        &self,
311        v: VariableId,
312        committed: &CommittedState,
313        txn: &Transaction,
314    ) -> usize {
315        self.use_sites.use_count(v)
316            + committed.added_count.get(&v).copied().unwrap_or(0)
317            + txn.added_delta.get(&v).copied().unwrap_or(0)
318            - committed.freed_count.get(&v).copied().unwrap_or(0)
319            - txn.freed_delta.get(&v).copied().unwrap_or(0)
320    }
321
322    /// Verifies that no non-copy variable ends up with more uses than it started
323    /// with. A rename `v → rep` adds a use of `rep`; this is only valid if `rep`'s
324    /// original use was freed (producer removed) in the same or earlier transaction.
325    ///
326    /// TODO(eytan-starkware): This check is overly conservative. Uses in mutually
327    /// exclusive branches (neither dominates the other) are safe even if they
328    /// increase the total count — at runtime the variable is consumed at most once.
329    /// Relax this to check that uses form an antichain in the dominance tree.
330    fn verify_non_copy_safety(&self, committed: &CommittedState, txn: &Transaction) -> bool {
331        txn.added_delta.iter().all(|(&v, _)| {
332            self.variables[v].info.copyable.is_ok()
333                || self.effective_use_count(v, committed, txn) <= self.use_sites.use_count(v)
334        })
335    }
336
337    /// Rewrites all usages of `from` to `to` at the given location.
338    fn rename_var(&mut self, loc: UseLocation, from: VariableId, to: VariableId) {
339        match loc {
340            UseLocation::Statement((block_id, stmt_idx)) => {
341                for input in self.blocks[block_id].statements[stmt_idx].inputs_mut() {
342                    if input.var_id == from {
343                        input.var_id = to;
344                    }
345                }
346            }
347            UseLocation::BlockEnd(block_id) => match &mut self.blocks[block_id].end {
348                BlockEnd::Return(inputs, _) => {
349                    for input in inputs {
350                        if input.var_id == from {
351                            input.var_id = to;
352                        }
353                    }
354                }
355                BlockEnd::Panic(input) => {
356                    if input.var_id == from {
357                        input.var_id = to;
358                    }
359                }
360                BlockEnd::Match { info } => {
361                    for input in info.inputs_mut() {
362                        if input.var_id == from {
363                            input.var_id = to;
364                        }
365                    }
366                }
367                BlockEnd::Goto(_, remapping) => {
368                    for src in remapping.values_mut() {
369                        if src.var_id == from {
370                            src.var_id = to;
371                        }
372                    }
373                }
374                BlockEnd::NotSet => {}
375            },
376        }
377    }
378
379    fn is_visible(&self, use_loc: UseLocation, var_id: VariableId) -> bool {
380        let Some(def) = self.definition(var_id) else {
381            return false;
382        };
383        let def_block = def.block();
384        let use_block = use_loc.block();
385        if def_block != use_block {
386            return self.dominators.dominates(def_block, use_block);
387        }
388        match (def, use_loc) {
389            (DefLocation::BlockEntry(_), _) => true,
390            (DefLocation::Statement(_), UseLocation::BlockEnd(_)) => true,
391            (DefLocation::Statement((_, di)), UseLocation::Statement((_, ui))) => ui > di,
392        }
393    }
394
395    fn is_committed(&self, def: DefLocation, committed: &CommittedState) -> bool {
396        match def {
397            DefLocation::Statement((block_id, stmt_idx)) => {
398                committed.is_stmt_removed(block_id, stmt_idx)
399            }
400            DefLocation::BlockEntry(_) => false,
401        }
402    }
403
404    fn is_removed(&self, def: DefLocation, committed: &CommittedState, txn: &Transaction) -> bool {
405        self.is_committed(def, committed) || txn.removed.contains(&def)
406    }
407
408    fn has_added_uses(&self, v: VariableId, committed: &CommittedState, txn: &Transaction) -> bool {
409        committed.added_count.contains_key(&v) || txn.added_delta.contains_key(&v)
410    }
411
412    fn definition(&self, var: VariableId) -> Option<DefLocation> {
413        self.def_sites[var.index()]
414    }
415
416    fn removable(&self, def: DefLocation) -> bool {
417        match def {
418            DefLocation::Statement(stmt_loc) => match self.blocks[stmt_loc] {
419                Statement::Const(_)
420                | Statement::StructConstruct(_)
421                | Statement::StructDestructure(_)
422                | Statement::EnumConstruct(_)
423                | Statement::Snapshot(_)
424                | Statement::Desnap(_)
425                | Statement::IntoBox(_)
426                | Statement::Unbox(_) => true,
427                Statement::Call(_) => false,
428            },
429            DefLocation::BlockEntry(_) => true,
430        }
431    }
432}