Skip to main content

cairo_lang_lowering/optimizations/
match_optimizer.rs

1#[cfg(test)]
2#[path = "match_optimizer_test.rs"]
3mod test;
4
5use cairo_lang_semantic::{ConcreteVariant, MatchArmSelector};
6use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
7use cairo_lang_utils::ordered_hash_set::OrderedHashSet;
8use cairo_lang_utils::unordered_hash_map::UnorderedHashMap;
9use itertools::{Itertools, zip_eq};
10use salsa::Database;
11
12use super::var_renamer::VarRenamer;
13use crate::analysis::def_site::{DefSiteAnalysis, DefSites};
14use crate::analysis::dominator::Dominators;
15use crate::analysis::equality_analysis::{EqualityAnalysis, EqualityState};
16use crate::analysis::{Analyzer, BackAnalysis, DefLocation, StatementLocation};
17use crate::borrow_check::Demand;
18use crate::borrow_check::demand::EmptyDemandReporter;
19use crate::ids::LocationId;
20use crate::utils::RebuilderEx;
21use crate::{
22    Block, BlockEnd, BlockId, Lowered, MatchArm, MatchEnumInfo, MatchInfo, Statement, VarRemapping,
23    VarUsage, VariableArena, VariableId,
24};
25
26pub type MatchOptimizerDemand<'db> = Demand<VariableId, (), ()>;
27
28impl<'db> MatchOptimizerDemand<'db> {
29    fn update(&mut self, statement: &Statement<'db>) {
30        self.variables_introduced(&mut EmptyDemandReporter {}, statement.outputs(), ());
31        self.variables_used(
32            &mut EmptyDemandReporter {},
33            statement.inputs().iter().map(|VarUsage { var_id, .. }| (var_id, ())),
34        );
35    }
36}
37
38/// Optimizes Statement::EnumConstruct that is followed by a match to jump to the target of the
39/// relevant match arm.
40///
41/// For example, given:
42///
43/// ```plain
44/// blk0:
45/// Statements:
46/// (v1: core::option::Option::<core::integer::u32>) <- Option::Some(v0)
47/// End:
48/// Goto(blk1, {v1-> v2})
49///
50/// blk1:
51/// Statements:
52/// End:
53/// Match(match_enum(v2) {
54///   Option::Some(v3) => blk4,
55///   Option::None(v4) => blk5,
56/// })
57/// ```
58///
59/// Change `blk0` to jump directly to `blk4`.
60pub fn optimize_matches<'db>(db: &'db dyn Database, lowered: &mut Lowered<'db>) {
61    if lowered.blocks.is_empty() {
62        return;
63    }
64    let ctx = MatchOptimizerContext::new(db, lowered);
65    let mut analysis = BackAnalysis::new(&*lowered, ctx);
66    analysis.get_root_info();
67
68    let mut new_blocks = vec![];
69    let mut next_block_id = BlockId(lowered.blocks.len());
70
71    // Track variable renaming that results from applying the fixes below.
72    // For each (variable_id, arm_idx) pair that is remapped (prior to the match),
73    // we assign a new variable (to satisfy the SSA requirement).
74    //
75    // For example, consider the following blocks:
76    //   blk0:
77    //   Statements:
78    //   (v0: test::Color) <- Color::Red(v5)
79    //   End:
80    //   Goto(blk1, {v1 -> v2, v0 -> v3})
81    //
82    //   blk1:
83    //   Statements:
84    //   End:
85    //   Match(match_enum(v3) {
86    //     Color::Red(v4) => blk2,
87    //   })
88    //
89    // When the optimization is applied, block0 will jump directly to blk2. Since the definition of
90    // v2 is at blk1, we must map v1 to a new variable.
91    //
92    // If there is another fix for the same match arm, the same variable will be used.
93    let mut var_renaming = UnorderedHashMap::<(VariableId, usize), VariableId>::default();
94
95    // Fixes were added in reverse order and need to be applied in that order.
96    // This is because `additional_remappings` in later blocks may need to be renamed by fixes from
97    // earlier blocks.
98    for fix in analysis.analyzer.fixes {
99        // Choose new variables for each destination of the additional remappings (see comment
100        // above).
101        let mut new_remapping = fix.remapping.clone();
102        let mut renamed_vars = OrderedHashMap::<VariableId, VariableId>::default();
103        for (var, dst) in fix.additional_remappings.iter() {
104            // Allocate a new variable, if it was not allocated before.
105            let new_var = *var_renaming
106                .entry((*var, fix.arm_idx))
107                .or_insert_with(|| lowered.variables.alloc(lowered.variables[*var].clone()));
108            new_remapping.insert(new_var, *dst);
109            renamed_vars.insert(*var, new_var);
110        }
111
112        let block = &mut lowered.blocks[fix.fix_block];
113        if let Some(UpstreamEnumConstruct { stmt_idx, n_same_block_statement, remove }) =
114            &fix.enum_construct
115        {
116            assert_eq!(
117                block.statements.len() - 1,
118                stmt_idx + n_same_block_statement,
119                "Unexpected number of statements in block."
120            );
121            if *remove {
122                block.statements.remove(*stmt_idx);
123            }
124        }
125
126        handle_additional_statements(
127            &mut lowered.variables,
128            &mut var_renaming,
129            &mut new_remapping,
130            &mut renamed_vars,
131            block,
132            &fix,
133        );
134
135        block.end = BlockEnd::Goto(fix.target_block, new_remapping);
136        if fix.fix_block == fix.match_block {
137            // The match was removed (by the assignment of `block.end` above), no need to fix it.
138            // Sanity check: there should be no additional remapping in this case.
139            assert!(fix.additional_remappings.remapping.is_empty());
140            continue;
141        }
142
143        let block = &mut lowered.blocks[fix.match_block];
144        let BlockEnd::Match { info: MatchInfo::Enum(MatchEnumInfo { arms, location, .. }) } =
145            &mut block.end
146        else {
147            unreachable!("match block should end with a match.");
148        };
149
150        let arm = arms.get_mut(fix.arm_idx).unwrap();
151        if fix.target_block != arm.block_id {
152            // The match arm was already fixed, no need to fix it again.
153            continue;
154        }
155
156        // Fix match arm not to jump directly to a block that has an incoming gotos and add
157        // remapping that matches the goto above.
158        let arm_var = arm.var_ids.get_mut(0).unwrap();
159        let orig_var = *arm_var;
160        *arm_var = lowered.variables.alloc(lowered.variables[orig_var].clone());
161        let mut new_block_remapping: VarRemapping<'_> = Default::default();
162
163        new_block_remapping.insert(orig_var, VarUsage { var_id: *arm_var, location: *location });
164        for (var, new_var) in renamed_vars.iter() {
165            new_block_remapping.insert(*new_var, VarUsage { var_id: *var, location: *location });
166        }
167
168        new_blocks.push(Block {
169            statements: vec![],
170            end: BlockEnd::Goto(arm.block_id, new_block_remapping),
171        });
172        arm.block_id = next_block_id;
173        next_block_id = next_block_id.next_block_id();
174
175        let mut var_renamer = VarRenamer { renamed_vars: renamed_vars.into_iter().collect() };
176        // Apply the variable renaming to the reachable blocks.
177        for block_id in fix.reachable_blocks {
178            let block = &mut lowered.blocks[block_id];
179            *block = var_renamer.rebuild_block(block);
180        }
181    }
182
183    for block in new_blocks {
184        lowered.blocks.push(block);
185    }
186}
187
188/// Handles the additional statements in the fix.
189///
190/// The additional statements are not in the same block as the enum construct so they need
191/// to be copied the current block with new outputs to keep the SSA property.
192/// further we need to remap and rename the outputs for the merge in `fix.target_block`.
193///
194/// Note that since the statements are copied this might increase the code size.
195fn handle_additional_statements<'db>(
196    variables: &mut VariableArena<'db>,
197    var_renaming: &mut UnorderedHashMap<(VariableId, usize), VariableId>,
198    new_remapping: &mut VarRemapping<'db>,
199    renamed_vars: &mut OrderedHashMap<VariableId, VariableId>,
200    block: &mut Block<'db>,
201    fix: &FixInfo<'db>,
202) {
203    if fix.additional_stmts.is_empty() {
204        return;
205    }
206
207    // Maps input in the original lowering to the inputs after the optimization.
208    // Since the statement are copied from after `additional_remappings` to before it,
209    // `inputs_remapping` is initialized with `additional_remapping`.
210    let mut inputs_remapping = UnorderedHashMap::<VariableId, VariableId>::from_iter(
211        fix.additional_remappings.iter().map(|(k, v)| (*k, v.var_id)),
212    );
213    for mut stmt in fix.additional_stmts.iter().cloned() {
214        for input in stmt.inputs_mut() {
215            if let Some(orig_var) = inputs_remapping.get(&input.var_id) {
216                input.var_id = *orig_var;
217            }
218        }
219
220        for output in stmt.outputs_mut() {
221            let orig_output = *output;
222            // Allocate a new variable for the output in the fixed block.
223            *output = variables.alloc(variables[*output].clone());
224            inputs_remapping.insert(orig_output, *output);
225
226            // Allocate a new post remapping output, if it was not allocated before.
227            let new_output = *var_renaming
228                .entry((orig_output, fix.arm_idx))
229                .or_insert_with(|| variables.alloc(variables[*output].clone()));
230            let location = variables[*output].location;
231            new_remapping.insert(new_output, VarUsage { var_id: *output, location });
232            renamed_vars.insert(orig_output, new_output);
233        }
234
235        block.statements.push(stmt);
236    }
237}
238
239/// Try to apply the optimization for the given variant.
240///
241/// `inner` is the value to feed into the matched arm. `fix_block` is the block whose `end`
242/// will be replaced with a goto. `enum_construct`, when present, gives `(stmt_idx,
243/// output_var)` of the upstream `Statement::EnumConstruct` in `fix_block` — `output_var` is
244/// used to decide whether the construct can be removed. `None` for equality-anchored folds.
245///
246/// The candidate is consumed: on success its arm-specific state is folded into the returned
247/// `FixInfo`; on failure it is dropped. Either way the caller cannot reuse it.
248fn try_get_fix_info<'db>(
249    variant: &ConcreteVariant<'db>,
250    inner: VarUsage<'db>,
251    enum_construct: Option<(usize, VariableId)>,
252    fix_block: BlockId,
253    info: &mut AnalysisInfo<'db, '_>,
254    mut candidate: OptimizationCandidate<'db, '_>,
255) -> Option<FixInfo<'db>> {
256    let (arm_idx, arm) = candidate
257        .match_arms
258        .iter()
259        .find_position(
260            |arm| matches!(&arm.arm_selector, MatchArmSelector::VariantId(v) if v == variant),
261        )
262        .expect("arm not found.");
263
264    let [var_id] = arm.var_ids.as_slice() else {
265        panic!("An arm of an EnumMatch should produce a single variable.");
266    };
267
268    // Prepare a remapping object for the input of the EnumConstruct, which will be used as `var_id`
269    // in `arm.block_id`.
270    let mut remapping = VarRemapping::default();
271    remapping.insert(*var_id, inner);
272
273    // Compute the demand based on the demand of the specific arm, rather than the current demand
274    // (which contains the union of the demands from all the arms).
275    // Apply the remapping of the input variable and the additional remappings if exist.
276    let mut demand = std::mem::take(&mut candidate.arm_demands[arm_idx]);
277
278    let additional_stmts = candidate
279        .statement_rev
280        .iter()
281        .rev()
282        .skip(candidate.n_same_block_statement)
283        .cloned()
284        .cloned()
285        .collect_vec();
286    for stmt in &additional_stmts {
287        demand.update(stmt);
288    }
289
290    demand
291        .apply_remapping(&mut EmptyDemandReporter {}, [(var_id, (&inner.var_id, ()))].into_iter());
292
293    let additional_remappings = match candidate.remapping {
294        Some(remappings) => {
295            // Filter the additional remappings to only include those that are used in relevant arm.
296            VarRemapping {
297                remapping: OrderedHashMap::from_iter(remappings.iter().filter_map(|(dst, src)| {
298                    if demand.vars.contains_key(dst) { Some((*dst, *src)) } else { None }
299                })),
300            }
301        }
302        None => VarRemapping::default(),
303    };
304
305    if !additional_remappings.is_empty() && candidate.future_merge {
306        // additional_remappings with a future merge cannot be folded.
307        return None;
308    }
309
310    demand.apply_remapping(
311        &mut EmptyDemandReporter {},
312        additional_remappings.iter().map(|(dst, src_var_usage)| (dst, (&src_var_usage.var_id, ()))),
313    );
314
315    for stmt in candidate.statement_rev.iter().rev() {
316        demand.update(stmt);
317    }
318    info.demand = demand;
319    info.reachable_blocks = std::mem::take(&mut candidate.arm_reachable_blocks[arm_idx]);
320
321    let enum_construct = enum_construct.map(|(stmt_idx, output)| UpstreamEnumConstruct {
322        stmt_idx,
323        n_same_block_statement: candidate.n_same_block_statement,
324        remove: !info.demand.vars.contains_key(&output),
325    });
326
327    Some(FixInfo {
328        fix_block,
329        match_block: candidate.match_block,
330        arm_idx,
331        target_block: arm.block_id,
332        remapping,
333        reachable_blocks: info.reachable_blocks.clone(),
334        additional_remappings,
335        enum_construct,
336        additional_stmts,
337    })
338}
339
340pub struct FixInfo<'db> {
341    /// The block whose `end` will be replaced with a goto to the chosen arm.
342    fix_block: BlockId,
343    /// The block with the match statement that we want to jump over.
344    match_block: BlockId,
345    /// The index of the arm that we want to jump to.
346    arm_idx: usize,
347    /// The target block to jump to.
348    target_block: BlockId,
349    /// Remaps the input of the enum construct to the variable that is introduced by the match arm.
350    remapping: VarRemapping<'db>,
351    /// The blocks that can be reached from the relevant arm of the match.
352    reachable_blocks: OrderedHashSet<BlockId>,
353    /// Additional remappings that appeared in a `Goto` leading to the match.
354    additional_remappings: VarRemapping<'db>,
355    /// When `Some`, an upstream `EnumConstruct` sits at `(fix_block, stmt_idx)`; it is
356    /// removed iff `remove`. `None` for equality-anchored folds (no local construct).
357    enum_construct: Option<UpstreamEnumConstruct>,
358    /// Additional statement that appear before the match but not in the same block as the enum
359    /// construct.
360    additional_stmts: Vec<Statement<'db>>,
361}
362
363/// Describes the upstream `EnumConstruct` in `fix_block` anchoring a construct-anchored fold.
364struct UpstreamEnumConstruct {
365    /// Index of the `EnumConstruct` statement in `fix_block`.
366    stmt_idx: usize,
367    /// Number of statements after the construct in the same block, used for an integrity
368    /// assertion.
369    n_same_block_statement: usize,
370    /// When true, the `EnumConstruct` is removed (input not demanded after the match).
371    /// When false, the construct stays (input is demanded elsewhere, only legal if droppable
372    /// because the fold may drop one use).
373    remove: bool,
374}
375
376#[derive(Clone)]
377struct OptimizationCandidate<'db, 'a> {
378    /// The variable that is matched.
379    match_variable: VariableId,
380
381    /// The match arms of the extern match that we are optimizing.
382    match_arms: &'a [MatchArm<'db>],
383
384    /// The block that the match is in.
385    match_block: BlockId,
386
387    /// The source location of the match (used to synthesize `VarUsage`s for equality-based fixes).
388    match_location: LocationId<'db>,
389
390    /// The demands at the arms.
391    arm_demands: Vec<MatchOptimizerDemand<'db>>,
392
393    /// Whether there is a future merge between the match arms.
394    future_merge: bool,
395
396    /// The blocks that can be reached from each of the arms.
397    arm_reachable_blocks: Vec<OrderedHashSet<BlockId>>,
398
399    /// A remappings that appeared in a `Goto` leading to the match.
400    /// Only one such remapping is allowed as this is typically the case
401    /// after running `optimize_remapping` and `reorder_statements` and it simplifies the
402    /// optimization.
403    remapping: Option<&'a VarRemapping<'db>>,
404
405    /// The statements before the match in reverse order.
406    statement_rev: Vec<&'a Statement<'db>>,
407
408    /// The number of statement in the in the same block as the enum construct.
409    n_same_block_statement: usize,
410}
411
412pub struct MatchOptimizerContext<'db, 'a> {
413    fixes: Vec<FixInfo<'db>>,
414    equalities: Vec<Option<EqualityState<'db>>>,
415    dominators: Dominators,
416    def_sites: DefSites,
417    /// Borrow of `lowered.variables`, used by the equality-fold gate to check
418    /// droppable/copyable.
419    variables: &'a VariableArena<'db>,
420}
421
422impl<'db, 'a> MatchOptimizerContext<'db, 'a> {
423    fn new(db: &'db dyn Database, lowered: &'a Lowered<'db>) -> Self {
424        Self {
425            fixes: vec![],
426            equalities: EqualityAnalysis::analyze(db, lowered),
427            dominators: Dominators::analyze(lowered),
428            def_sites: DefSiteAnalysis::analyze(lowered),
429            variables: &lowered.variables,
430        }
431    }
432
433    /// Looks up the known variant of `var` at `block_id`'s exit via equality analysis.
434    fn get_enum_variant(
435        &mut self,
436        block_id: BlockId,
437        var: VariableId,
438    ) -> Option<(ConcreteVariant<'db>, VariableId)> {
439        self.equalities[block_id.0].as_mut()?.get_enum_construct(var)
440    }
441
442    /// Returns true if `var`'s definition is in scope at the end of `block_id`.
443    fn is_visible_at_block_end(&self, var_id: VariableId, at: BlockId) -> bool {
444        let Some(loc) = self.def_sites[var_id.index()] else { return false };
445        let block_id = match loc {
446            DefLocation::Statement((b, _)) | DefLocation::BlockEntry(b) => b,
447        };
448        self.dominators.dominates(block_id, at)
449    }
450}
451
452#[derive(Clone)]
453pub struct AnalysisInfo<'db, 'a> {
454    candidate: Option<OptimizationCandidate<'db, 'a>>,
455    demand: MatchOptimizerDemand<'db>,
456    /// Blocks that can be reached from the current block.
457    reachable_blocks: OrderedHashSet<BlockId>,
458}
459
460impl<'db: 'a, 'a> Analyzer<'db, 'a> for MatchOptimizerContext<'db, 'a> {
461    type Info = AnalysisInfo<'db, 'a>;
462
463    fn visit_block_start(&mut self, info: &mut Self::Info, block_id: BlockId, _block: &Block<'db>) {
464        info.reachable_blocks.insert(block_id);
465    }
466
467    fn visit_stmt(
468        &mut self,
469        info: &mut Self::Info,
470        statement_location: StatementLocation,
471        stmt: &'a Statement<'db>,
472    ) {
473        if let Some(mut candidate) = info.candidate.take() {
474            match stmt {
475                Statement::EnumConstruct(enum_construct_stmt)
476                    if enum_construct_stmt.output == candidate.match_variable =>
477                {
478                    if let Some(fix_info) = try_get_fix_info(
479                        &enum_construct_stmt.variant,
480                        enum_construct_stmt.input,
481                        Some((statement_location.1, enum_construct_stmt.output)),
482                        statement_location.0,
483                        info,
484                        candidate,
485                    ) {
486                        self.fixes.push(fix_info);
487                        return;
488                    }
489                }
490                _ => {
491                    candidate.statement_rev.push(stmt);
492                    candidate.n_same_block_statement += 1;
493                    info.candidate = Some(candidate);
494                }
495            }
496        }
497
498        info.demand.update(stmt);
499    }
500
501    fn visit_goto(
502        &mut self,
503        info: &mut Self::Info,
504        _statement_location: StatementLocation,
505        _target_block_id: BlockId,
506        remapping: &'a VarRemapping<'db>,
507    ) {
508        info.demand.apply_remapping(
509            &mut EmptyDemandReporter {},
510            remapping.iter().map(|(dst, src)| (dst, (&src.var_id, ()))),
511        );
512
513        let Some(candidate) = &mut info.candidate else {
514            return;
515        };
516
517        // New block, so reset the number of statements that are in the same block as the enum
518        // construct.
519        candidate.n_same_block_statement = 0;
520
521        if candidate.future_merge
522            && candidate.statement_rev.iter().any(|stmt| !stmt.outputs().is_empty())
523        {
524            // If we have a future merge and a statement not in the same block as the enum construct
525            // has an output, we cannot apply the optimization.
526            info.candidate = None;
527            return;
528        }
529
530        if remapping.is_empty() {
531            return;
532        }
533
534        if candidate.remapping.is_some() {
535            info.candidate = None;
536            return;
537        }
538
539        // Store the goto's remapping.
540        candidate.remapping = Some(remapping);
541        if let Some(var_usage) = remapping.get(&candidate.match_variable) {
542            candidate.match_variable = var_usage.var_id;
543        }
544    }
545
546    fn merge_match(
547        &mut self,
548        (block_id, _statement_idx): StatementLocation,
549        match_info: &'a MatchInfo<'db>,
550        infos: impl Iterator<Item = Self::Info>,
551    ) -> Self::Info {
552        let (arm_demands, arm_reachable_blocks): (Vec<_>, Vec<_>) =
553            infos.map(|info| (info.demand, info.reachable_blocks)).unzip();
554
555        let arm_demands_without_arm_var = zip_eq(match_info.arms(), &arm_demands)
556            .map(|(arm, demand)| {
557                let mut demand = demand.clone();
558                // Remove the variable that is introduced by the match arm.
559                demand.variables_introduced(&mut EmptyDemandReporter {}, &arm.var_ids, ());
560
561                (demand, ())
562            })
563            .collect_vec();
564        let mut demand = MatchOptimizerDemand::merge_demands(
565            &arm_demands_without_arm_var,
566            &mut EmptyDemandReporter {},
567        );
568
569        // Union the reachable blocks for all the infos.
570        let mut reachable_blocks = OrderedHashSet::default();
571        let mut max_possible_size = 0;
572        for cur_reachable_blocks in &arm_reachable_blocks {
573            reachable_blocks.extend(cur_reachable_blocks.iter().cloned());
574            max_possible_size += cur_reachable_blocks.len();
575        }
576        // If the size of `reachable_blocks` is less than the sum of the sizes of the
577        // `arm_reachable_blocks`, then there was a collision.
578        let found_collision = reachable_blocks.len() < max_possible_size;
579
580        // A match on an Enum is a fold candidate under one of the following cases:
581        // (a) Input not demanded after the match — the construct-anchored fold (`visit_stmt`)
582        //     may also remove the upstream construct.
583        // (b) Input demanded but droppable — the matched value is also consumed elsewhere
584        //     (must be Copy, else SSA would already be invalid). The construct-anchored fold
585        //     still applies, but the construct stays for the other consumers.
586        let candidate = match match_info {
587            MatchInfo::Enum(MatchEnumInfo { input, arms, location, .. })
588                if !demand.vars.contains_key(&input.var_id)
589                    || self.variables[input.var_id].info.droppable.is_ok() =>
590            {
591                let candidate = OptimizationCandidate {
592                    match_variable: input.var_id,
593                    match_arms: arms,
594                    match_block: block_id,
595                    match_location: *location,
596                    arm_demands,
597                    future_merge: found_collision,
598                    arm_reachable_blocks,
599                    remapping: None,
600                    statement_rev: vec![],
601                    n_same_block_statement: 0,
602                };
603                // If equality analysis already knows the variant at this block's exit, fold the
604                // match into a direct goto here. The matched-arm payload comes from equality
605                // analysis, not from a local construct; any newly-dead EnumConstruct upstream
606                // will be cleaned up by later passes.
607                if let Some((variant, payload_var)) =
608                    self.get_enum_variant(block_id, candidate.match_variable)
609                    // Without a construct to remove upstream, a non-droppable matched value
610                    // would leak (the match was its only consumer on this path).
611                    && self.variables[candidate.match_variable].info.droppable.is_ok()
612                    // Needs to be copy if we are introducing a new use.
613                    && self.variables[payload_var].info.copyable.is_ok()
614                {
615                    // Equality analysis only records an enum construct at blocks where the
616                    // payload definition dominates, so this must hold.
617                    assert!(self.is_visible_at_block_end(payload_var, block_id));
618                    let mut info = Self::Info {
619                        candidate: None,
620                        demand: MatchOptimizerDemand::default(),
621                        reachable_blocks: reachable_blocks.clone(),
622                    };
623                    // `try_get_fix_info` only fails when there are additional remappings, which
624                    // never exist for an equality-anchored fold (no goto observed yet on the
625                    // freshly-built candidate).
626                    let fix_info = try_get_fix_info(
627                        &variant,
628                        VarUsage { var_id: payload_var, location: candidate.match_location },
629                        None,
630                        block_id,
631                        &mut info,
632                        candidate,
633                    )
634                    .expect("equality-fold has no additional remappings, cannot fail");
635                    self.fixes.push(fix_info);
636                    return info;
637                }
638                Some(candidate)
639            }
640            _ => None,
641        };
642
643        demand.variables_used(
644            &mut EmptyDemandReporter {},
645            match_info.inputs().iter().map(|VarUsage { var_id, .. }| (var_id, ())),
646        );
647
648        Self::Info { candidate, demand, reachable_blocks }
649    }
650
651    fn info_from_return(
652        &mut self,
653        _statement_location: StatementLocation,
654        vars: &[VarUsage<'db>],
655    ) -> Self::Info {
656        let mut demand = MatchOptimizerDemand::default();
657        demand.variables_used(
658            &mut EmptyDemandReporter {},
659            vars.iter().map(|VarUsage { var_id, .. }| (var_id, ())),
660        );
661        Self::Info { candidate: None, demand, reachable_blocks: Default::default() }
662    }
663}