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/non-drop 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            let info = &self.variables[var_id].info;
236            if (info.copyable.is_err() || info.droppable.is_err())
237                && self.effective_use_count(var_id, committed, txn) == 0
238            {
239                // A variable with no remaining consumers must not be orphaned: if
240                // non-copy, renames targeting it would create duplicate consuming
241                // uses; if non-drop, leaving it unconsumed is illegal. Either way try
242                // to remove the producer. If the cascade fails, propagate failure so
243                // the whole transaction is discarded; partial cascade state is left in
244                // `txn` and will be dropped together with the rest by the worklist
245                // when this top-level `try_remove` returns false.
246                let Some(producer) = self.definition(var_id) else {
247                    continue;
248                };
249                if !self.try_remove(producer, tried, committed, txn) {
250                    return false;
251                }
252            }
253        }
254        true
255    }
256
257    /// Returns the renames needed to forward all uses of `v` to its representative,
258    /// or `None` if any use cannot be forwarded.
259    /// Non-consuming uses (snapshots) are included — they need renaming too.
260    fn forwarding_dependencies(
261        &self,
262        v: VariableId,
263        committed: &CommittedState,
264        txn: &Transaction,
265    ) -> Option<Vec<RenameDep>> {
266        let mut deps = Vec::new();
267        for (loc, count) in self.use_sites.use_locs(v) {
268            if let UseLocation::Statement(sl) = loc
269                && self.is_removed(DefLocation::Statement(sl), committed, txn)
270            {
271                continue;
272            }
273            let eq_state = self.equalities[loc.block().0].as_ref()?;
274            // Snapshots are NOT rejected — they generate renames like any other use.
275            let rep = eq_state.find_immut(v);
276            if rep == v || !self.is_visible(loc, rep) {
277                return None;
278            }
279            deps.push(RenameDep { loc, from: v, to: rep, count });
280        }
281        // Fail if v gained uses from renames. These added uses aren't tracked in
282        // use_sites, so the renames above wouldn't cover them — removing v's
283        // producer would leave dangling references.
284        if self.has_added_uses(v, committed, txn) {
285            return None;
286        }
287        Some(deps)
288    }
289
290    /// Checks ALL outputs of a node. Returns combined rename list or `None`.
291    fn try_forward_all_outputs(
292        &self,
293        def: DefLocation,
294        committed: &CommittedState,
295        txn: &Transaction,
296    ) -> Option<Vec<RenameDep>> {
297        if !self.removable(def) {
298            return None;
299        }
300        // TODO(eytan-starkware): Handle BlockEntry from goto remappings.
301        let DefLocation::Statement(sl) = def else { return None };
302        let outputs: Vec<VariableId> = self.blocks[sl].outputs().to_vec();
303        let mut all_renames = Vec::new();
304        for v in outputs {
305            all_renames.extend(self.forwarding_dependencies(v, committed, txn)?);
306        }
307        Some(all_renames)
308    }
309
310    /// Effective use count: original minus freed plus added.
311    fn effective_use_count(
312        &self,
313        v: VariableId,
314        committed: &CommittedState,
315        txn: &Transaction,
316    ) -> usize {
317        self.use_sites.use_count(v)
318            + committed.added_count.get(&v).copied().unwrap_or(0)
319            + txn.added_delta.get(&v).copied().unwrap_or(0)
320            - committed.freed_count.get(&v).copied().unwrap_or(0)
321            - txn.freed_delta.get(&v).copied().unwrap_or(0)
322    }
323
324    /// Verifies that no non-copy variable ends up with more uses than it started
325    /// with. A rename `v → rep` adds a use of `rep`; this is only valid if `rep`'s
326    /// original use was freed (producer removed) in the same or earlier transaction.
327    ///
328    /// TODO(eytan-starkware): This check is overly conservative. Uses in mutually
329    /// exclusive branches (neither dominates the other) are safe even if they
330    /// increase the total count — at runtime the variable is consumed at most once.
331    /// Relax this to check that uses form an antichain in the dominance tree.
332    fn verify_non_copy_safety(&self, committed: &CommittedState, txn: &Transaction) -> bool {
333        txn.added_delta.iter().all(|(&v, _)| {
334            self.variables[v].info.copyable.is_ok()
335                || self.effective_use_count(v, committed, txn) <= self.use_sites.use_count(v)
336        })
337    }
338
339    /// Rewrites all usages of `from` to `to` at the given location.
340    fn rename_var(&mut self, loc: UseLocation, from: VariableId, to: VariableId) {
341        match loc {
342            UseLocation::Statement((block_id, stmt_idx)) => {
343                for input in self.blocks[block_id].statements[stmt_idx].inputs_mut() {
344                    if input.var_id == from {
345                        input.var_id = to;
346                    }
347                }
348            }
349            UseLocation::BlockEnd(block_id) => match &mut self.blocks[block_id].end {
350                BlockEnd::Return(inputs, _) => {
351                    for input in inputs {
352                        if input.var_id == from {
353                            input.var_id = to;
354                        }
355                    }
356                }
357                BlockEnd::Panic(input) => {
358                    if input.var_id == from {
359                        input.var_id = to;
360                    }
361                }
362                BlockEnd::Match { info } => {
363                    for input in info.inputs_mut() {
364                        if input.var_id == from {
365                            input.var_id = to;
366                        }
367                    }
368                }
369                BlockEnd::Goto(_, remapping) => {
370                    for src in remapping.values_mut() {
371                        if src.var_id == from {
372                            src.var_id = to;
373                        }
374                    }
375                }
376                BlockEnd::NotSet => {}
377            },
378        }
379    }
380
381    fn is_visible(&self, use_loc: UseLocation, var_id: VariableId) -> bool {
382        let Some(def) = self.definition(var_id) else {
383            return false;
384        };
385        let def_block = def.block();
386        let use_block = use_loc.block();
387        if def_block != use_block {
388            return self.dominators.dominates(def_block, use_block);
389        }
390        match (def, use_loc) {
391            (DefLocation::BlockEntry(_), _) => true,
392            (DefLocation::Statement(_), UseLocation::BlockEnd(_)) => true,
393            (DefLocation::Statement((_, di)), UseLocation::Statement((_, ui))) => ui > di,
394        }
395    }
396
397    fn is_committed(&self, def: DefLocation, committed: &CommittedState) -> bool {
398        match def {
399            DefLocation::Statement((block_id, stmt_idx)) => {
400                committed.is_stmt_removed(block_id, stmt_idx)
401            }
402            DefLocation::BlockEntry(_) => false,
403        }
404    }
405
406    fn is_removed(&self, def: DefLocation, committed: &CommittedState, txn: &Transaction) -> bool {
407        self.is_committed(def, committed) || txn.removed.contains(&def)
408    }
409
410    fn has_added_uses(&self, v: VariableId, committed: &CommittedState, txn: &Transaction) -> bool {
411        committed.added_count.contains_key(&v) || txn.added_delta.contains_key(&v)
412    }
413
414    fn definition(&self, var: VariableId) -> Option<DefLocation> {
415        self.def_sites[var.index()]
416    }
417
418    fn removable(&self, def: DefLocation) -> bool {
419        match def {
420            DefLocation::Statement(stmt_loc) => match self.blocks[stmt_loc] {
421                Statement::Const(_)
422                | Statement::StructConstruct(_)
423                | Statement::StructDestructure(_)
424                | Statement::EnumConstruct(_)
425                | Statement::Snapshot(_)
426                | Statement::Desnap(_)
427                | Statement::IntoBox(_)
428                | Statement::Unbox(_) => true,
429                Statement::Call(_) => false,
430            },
431            DefLocation::BlockEntry(_) => true,
432        }
433    }
434}