mz_transform/equivalence_propagation.rs
1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Propagates expression equivalence from leaves to root, and back down again.
11//!
12//! Expression equivalences are `MirScalarExpr` replacements by simpler expressions.
13//! These equivalences derive from
14//! Filters: predicates must evaluate to `Datum::True`.
15//! Maps: new columns equal the expressions that define them.
16//! Joins: equated columns must be equal.
17//! Others: lots of other predicates we might learn (range constraints on aggregates; non-negativity)
18//!
19//! From leaf to root the equivalences are *enforced*, and communicate that the expression will not produce rows that do not satisfy the equivalence.
20//! From root to leaf the equivalences are *advised*, and communicate that the expression may discard any outputs that do not satisfy the equivalence.
21//!
22//! Importantly, in descent the operator *may not* assume any equivalence filtering will be applied to its results.
23//! It cannot therefore produce rows it would otherwise not, even rows that do not satisfy the equivalence.
24//! Operators *may* introduce filtering in descent, and they must do so to take further advantage of the equivalences.
25//!
26//! The subtlety is due to the expressions themselves causing the equivalences, and allowing more rows may invalidate equivalences.
27//! For example, we might learn that `Column(7)` equals `Literal(3)`, but must refrain from introducing that substitution in descent,
28//! because it is possible that the equivalence derives from restrictions in the expression we are visiting. Were we certain that the
29//! equivalence was independent of the expression (e.g. through a more nuanced expression traversal) we could imaging relaxing this.
30
31use std::collections::BTreeMap;
32
33use mz_expr::{Id, MirRelationExpr, MirScalarExpr};
34use mz_repr::Datum;
35
36use crate::analysis::equivalences::{EquivalenceClasses, Equivalences, ExpressionReducer};
37use crate::analysis::{Arity, DerivedView, RelationType};
38
39use crate::{TransformCtx, TransformError};
40
41/// Pulls up and pushes down predicate information represented as equivalences
42#[derive(Debug, Default)]
43pub struct EquivalencePropagation;
44
45impl crate::Transform for EquivalencePropagation {
46 fn name(&self) -> &'static str {
47 "EquivalencePropagation"
48 }
49
50 #[mz_ore::instrument(
51 target = "optimizer"
52 level = "trace",
53 fields(path.segment = "equivalence_propagation")
54 )]
55 fn actually_perform_transform(
56 &self,
57 relation: &mut MirRelationExpr,
58 ctx: &mut TransformCtx,
59 ) -> Result<(), TransformError> {
60 // Perform bottom-up equivalence class analysis.
61 use crate::analysis::DerivedBuilder;
62 let mut builder = DerivedBuilder::new(ctx.features);
63 builder.require(Equivalences);
64 let derived = builder.visit(relation);
65 let derived = derived.as_view();
66
67 let prior = relation.clone();
68
69 let mut get_equivalences = BTreeMap::default();
70 self.apply(
71 relation,
72 derived,
73 EquivalenceClasses::default(),
74 &mut get_equivalences,
75 ctx,
76 );
77
78 // Trace the plan as the result of `equivalence_propagation` before potentially applying
79 // `ColumnKnowledge`. (If `ColumnKnowledge` runs, it will trace its own result.)
80 mz_repr::explain::trace_plan(&*relation);
81
82 if prior == *relation {
83 let ck = crate::ColumnKnowledge::default();
84 ck.transform(relation, ctx)?;
85 if prior != *relation {
86 // This used to be tracing::error, but it became too common with
87 // dequadratic_eqprop_map.
88 tracing::warn!(
89 ?ctx.global_id,
90 "ColumnKnowledge performed work after EquivalencePropagation",
91 );
92 }
93 }
94
95 Ok(())
96 }
97}
98
99impl EquivalencePropagation {
100 /// Provides the opportunity to mutate `relation` in response to equivalences enforced by others.
101 ///
102 /// Provides the opportunity to mutate `relation` in response to equivalences enforced by their children,
103 /// as presented in `derived`, and equivalences enforced of their output (by their ancestors), as presented
104 /// in `outer_equivalences` and `get_equivalences`.
105 ///
106 /// The mutations should never invalidate an equivalence the operator has been reported as providing, as that
107 /// information may have already been acted upon by others.
108 ///
109 /// The `expr_index` argument must equal `expr`s position in post-order, so that it can be used as a reference
110 /// into `derived`. The argument can be used with the `SubtreeSize` analysis to determine the range of values
111 /// associated with `expr`.
112 ///
113 /// After the call, `get_equivalences` will be populated with certainly equivalences that will be certainly
114 /// enforced for all uses of each identifier. The information can be harvested and propagated to the definitions
115 /// of those identifiers.
116 pub fn apply(
117 &self,
118 expr: &mut MirRelationExpr,
119 derived: DerivedView,
120 mut outer_equivalences: EquivalenceClasses,
121 get_equivalences: &mut BTreeMap<Id, EquivalenceClasses>,
122 ctx: &mut TransformCtx,
123 ) {
124 // TODO: The top-down traversal can be coded as a worklist, with arguments tupled and enqueued.
125 // This has the potential to do a lot more cloning (of `outer_equivalences`), and some care is needed
126 // for `get_equivalences` which would be scoped to the whole method rather than tupled and enqueued.
127
128 let expr_type = derived
129 .value::<RelationType>()
130 .expect("RelationType required");
131 assert!(expr_type.is_some());
132 let expr_equivalences = derived
133 .value::<Equivalences>()
134 .expect("Equivalences required");
135
136 // `None` analysis values indicate collections that can be pruned.
137 let expr_equivalences = if let Some(e) = expr_equivalences {
138 e
139 } else {
140 expr.take_safely_with_col_types(expr_type.clone().unwrap());
141 return;
142 };
143
144 // Optimize `outer_equivalences` in the context of `expr_type`.
145 // If it ends up unsatisfiable, we can replace `expr` with an empty constant of the same relation type.
146 let reducer = expr_equivalences.reducer();
147 for class in outer_equivalences.classes.iter_mut() {
148 for expr in class.iter_mut() {
149 reducer.reduce_expr(expr);
150 }
151 }
152
153 outer_equivalences.minimize(expr_type.as_ref().map(|x| &x[..]));
154 if outer_equivalences.unsatisfiable() {
155 expr.take_safely_with_col_types(expr_type.clone().unwrap());
156 return;
157 }
158
159 match expr {
160 MirRelationExpr::Constant { rows, typ: _ } => {
161 if let Ok(rows) = rows {
162 let mut datum_vec = mz_repr::DatumVec::new();
163 // Delete any rows that violate the equivalences.
164 // Do not delete rows that produce errors, as they are semantically important.
165 rows.retain(|(row, _count)| {
166 let temp_storage = mz_repr::RowArena::new();
167 let datums = datum_vec.borrow_with(row);
168 outer_equivalences.classes.iter().all(|class| {
169 // Any subset of `Ok` results must equate, or we can drop the row.
170 let mut oks = class
171 .iter()
172 .filter_map(|e| e.eval(&datums[..], &temp_storage).ok());
173 if let Some(e1) = oks.next() {
174 oks.all(|e2| e1 == e2)
175 } else {
176 true
177 }
178 })
179 });
180 }
181 }
182 MirRelationExpr::Get { id, .. } => {
183 // Install and intersect with other equivalences from other `Get` sites.
184 // These will be read out by the corresponding `Let` binding's `value`.
185 if let Some(equivs) = get_equivalences.get_mut(id) {
186 *equivs = equivs.union(&outer_equivalences);
187 } else {
188 get_equivalences.insert(*id, outer_equivalences);
189 }
190 }
191 MirRelationExpr::Let { id, .. } => {
192 let id = *id;
193 // Traverse `body` first to assemble equivalences to push to `value`.
194 // Descend without a key for `id`, treating the absence as the identity for union.
195 // `Get` nodes with identifier `id` will populate the equivalence classes with the intersection of their guarantees.
196 let mut children_rev = expr.children_mut().rev().zip(derived.children_rev());
197
198 let body = children_rev.next().unwrap();
199 let value = children_rev.next().unwrap();
200
201 self.apply(
202 body.0,
203 body.1,
204 outer_equivalences.clone(),
205 get_equivalences,
206 ctx,
207 );
208
209 // We expect to find `id` in `get_equivalences`, as otherwise the binding is
210 // not referenced and can be removed.
211 if let Some(equivalences) = get_equivalences.get(&Id::Local(id)) {
212 self.apply(
213 value.0,
214 value.1,
215 equivalences.clone(),
216 get_equivalences,
217 ctx,
218 );
219 }
220 }
221 MirRelationExpr::LetRec { .. } => {
222 let mut child_iter = expr.children_mut().rev().zip(derived.children_rev());
223 // Continue in `body` with the outer equivalences.
224 let (body, view) = child_iter.next().unwrap();
225 self.apply(body, view, outer_equivalences, get_equivalences, ctx);
226 // Continue recursively, but without the outer equivalences supplied to `body`.
227 for (child, view) in child_iter {
228 self.apply(
229 child,
230 view,
231 EquivalenceClasses::default(),
232 get_equivalences,
233 ctx,
234 );
235 }
236 }
237 MirRelationExpr::Project { input, outputs } => {
238 // Transform `outer_equivalences` to one relevant for `input`.
239 outer_equivalences.permute(outputs);
240 self.apply(
241 input,
242 derived.last_child(),
243 outer_equivalences,
244 get_equivalences,
245 ctx,
246 );
247 }
248 MirRelationExpr::Map { input, scalars } => {
249 // Optimize `scalars` with respect to input equivalences.
250 let input_equivalences = derived
251 .last_child()
252 .value::<Equivalences>()
253 .expect("Equivalences required");
254
255 if let Some(input_equivalences_orig) = input_equivalences {
256 // We clone `input_equivalences` only if we want to modify it, which is when
257 // `enable_dequadratic_eqprop_map` is off. Otherwise, we work with the original
258 // `input_equivalences`.
259 let mut input_equivalences_cloned = None;
260 if !ctx.features.enable_dequadratic_eqprop_map {
261 // We mutate them for variadic Maps if the feature flag is not set.
262 input_equivalences_cloned = Some(input_equivalences_orig.clone());
263 }
264 // Get all output types, to reveal a prefix to each scaler expr.
265 let input_types = derived
266 .value::<RelationType>()
267 .expect("RelationType required")
268 .as_ref()
269 .unwrap();
270 let input_arity = input_types.len() - scalars.len();
271 for (index, expr) in scalars.iter_mut().enumerate() {
272 let reducer = if !ctx.features.enable_dequadratic_eqprop_map {
273 input_equivalences_cloned
274 .as_ref()
275 .expect("always filled if feature flag is not set")
276 .reducer()
277 } else {
278 input_equivalences_orig.reducer()
279 };
280 let changed = reducer.reduce_expr(expr);
281 if changed || !ctx.features.enable_less_reduce_in_eqprop {
282 expr.reduce(&input_types[..(input_arity + index)]);
283 }
284 if !ctx.features.enable_dequadratic_eqprop_map {
285 // Unfortunately, we had to stop doing the following, because it
286 // was making the `Map` handling quadratic.
287 // TODO: Get back to this when we have e-graphs.
288 // https://github.com/MaterializeInc/database-issues/issues/9157
289 //
290 // Introduce the fact relating the mapped expression and corresponding
291 // column. This allows subsequent expressions to be optimized with this
292 // information.
293 input_equivalences_cloned
294 .as_mut()
295 .expect("always filled if feature flag is not set")
296 .classes
297 .push(vec![
298 expr.clone(),
299 MirScalarExpr::column(input_arity + index),
300 ]);
301 input_equivalences_cloned
302 .as_mut()
303 .expect("always filled if feature flag is not set")
304 .minimize(Some(input_types));
305 }
306 }
307 let input_arity = *derived
308 .last_child()
309 .value::<Arity>()
310 .expect("Arity required");
311 outer_equivalences.project(0..input_arity);
312 self.apply(
313 input,
314 derived.last_child(),
315 outer_equivalences,
316 get_equivalences,
317 ctx,
318 );
319 }
320 }
321 MirRelationExpr::FlatMap { input, exprs, .. } => {
322 // Transform `exprs` by guarantees from `input` *and* from `outer`???
323 let input_equivalences = derived
324 .last_child()
325 .value::<Equivalences>()
326 .expect("Equivalences required");
327
328 if let Some(input_equivalences) = input_equivalences {
329 let input_types = derived
330 .last_child()
331 .value::<RelationType>()
332 .expect("RelationType required");
333 let reducer = input_equivalences.reducer();
334 for expr in exprs.iter_mut() {
335 let changed = reducer.reduce_expr(expr);
336 if changed || !ctx.features.enable_less_reduce_in_eqprop {
337 expr.reduce(input_types.as_ref().unwrap());
338 }
339 }
340 let input_arity = *derived
341 .last_child()
342 .value::<Arity>()
343 .expect("Arity required");
344 outer_equivalences.project(0..input_arity);
345 self.apply(
346 input,
347 derived.last_child(),
348 outer_equivalences,
349 get_equivalences,
350 ctx,
351 );
352 }
353 }
354 MirRelationExpr::Filter { input, predicates } => {
355 // Transform `predicates` by guarantees from `input` *and* from `outer`???
356 // If we reduce based on `input` guarantees, we won't be able to push those
357 // constraints down into input, which may be fine but is worth considering.
358 let input_equivalences = derived
359 .last_child()
360 .value::<Equivalences>()
361 .expect("Equivalences required");
362 if let Some(input_equivalences) = input_equivalences {
363 let input_types = derived
364 .last_child()
365 .value::<RelationType>()
366 .expect("RelationType required");
367 let reducer = input_equivalences.reducer();
368 for expr in predicates.iter_mut() {
369 let changed = reducer.reduce_expr(expr);
370 if changed || !ctx.features.enable_less_reduce_in_eqprop {
371 expr.reduce(input_types.as_ref().unwrap());
372 }
373 }
374 // Incorporate `predicates` into `outer_equivalences`.
375 let mut class = predicates.clone();
376 class.push(MirScalarExpr::literal_ok(
377 Datum::True,
378 mz_repr::ScalarType::Bool,
379 ));
380 outer_equivalences.classes.push(class);
381 outer_equivalences.minimize(input_types.as_ref().map(|x| &x[..]));
382 self.apply(
383 input,
384 derived.last_child(),
385 outer_equivalences,
386 get_equivalences,
387 ctx,
388 );
389 }
390 }
391
392 MirRelationExpr::Join {
393 inputs,
394 equivalences,
395 ..
396 } => {
397 // Certain equivalences are ensured by each of the inputs.
398 // Other equivalences are imposed by parents of the expression.
399 // We must not weaken the properties provided by the expression to its parents,
400 // meaning we can optimize `equivalences` with respect to input guarantees,
401 // but not with respect to `outer_equivalences`.
402
403 // Each child can be presented with the integration of `join_equivalences`, `outer_equivalences`,
404 // and each input equivalence *other than* their own, projected onto the input's columns.
405
406 // Enumerate locations to find each child's analysis outputs.
407 let mut children: Vec<_> = derived.children_rev().collect::<Vec<_>>();
408 children.reverse();
409
410 // Assemble the appended input types, for use in expression minimization.
411 // Do not use `expr_types`, which may reflect nullability that does not hold for the inputs.
412 let mut input_types = Some(
413 children
414 .iter()
415 .flat_map(|c| {
416 c.value::<RelationType>()
417 .expect("RelationType required")
418 .as_ref()
419 .unwrap()
420 .iter()
421 .cloned()
422 })
423 .collect::<Vec<_>>(),
424 );
425
426 // For each child, assemble its equivalences using join-relative column numbers.
427 // Don't do anything with the children yet, as we'll want to revisit each with
428 // this information at hand.
429 let mut columns = 0;
430 let mut input_equivalences = Vec::with_capacity(children.len());
431 for child in children.iter() {
432 let child_arity = child.value::<Arity>().expect("Arity required");
433 let equivalences = child
434 .value::<Equivalences>()
435 .expect("Equivalences required")
436 .clone();
437
438 if let Some(mut equivalences) = equivalences {
439 let permutation = (columns..(columns + child_arity)).collect::<Vec<_>>();
440 equivalences.permute(&permutation);
441 equivalences.minimize(input_types.as_ref().map(|x| &x[..]));
442 input_equivalences.push(equivalences);
443 }
444 columns += child_arity;
445 }
446
447 // Form the equivalences we will use to replace `equivalences`.
448 let mut join_equivalences = EquivalenceClasses::default();
449 join_equivalences
450 .classes
451 .extend(equivalences.iter().cloned());
452 // // Optionally, introduce `outer_equivalences` into `equivalences`.
453 // // This is not required, but it could be very helpful. To be seen.
454 // join_equivalences
455 // .classes
456 // .extend(outer_equivalences.classes.clone());
457
458 // Reduce join equivalences by the input equivalences.
459 for input_equivs in input_equivalences.iter() {
460 let reducer = input_equivs.reducer();
461 for class in join_equivalences.classes.iter_mut() {
462 for expr in class.iter_mut() {
463 // Semijoin elimination currently fails if you do more advanced simplification than
464 // literal substitution.
465 let old = expr.clone();
466 let changed = reducer.reduce_expr(expr);
467 let acceptable_sub = literal_domination(&old, expr);
468 if changed || !ctx.features.enable_less_reduce_in_eqprop {
469 expr.reduce(input_types.as_ref().unwrap());
470 }
471 if !acceptable_sub && !literal_domination(&old, expr) {
472 expr.clone_from(&old);
473 }
474 }
475 }
476 }
477 // Remove nullability information, as it has already been incorporated from input equivalences,
478 // and if it was reduced out relative to input equivalences we don't want to re-introduce it.
479 if let Some(input_types) = input_types.as_mut() {
480 for col in input_types.iter_mut() {
481 col.nullable = true;
482 }
483 }
484 join_equivalences.minimize(input_types.as_ref().map(|x| &x[..]));
485
486 // Revisit each child, determining the information to present to it, and recurring.
487 let mut columns = 0;
488 for ((index, child), expr) in
489 children.into_iter().enumerate().zip(inputs.iter_mut())
490 {
491 let child_arity = child.value::<Arity>().expect("Arity required");
492
493 let mut push_equivalences = join_equivalences.clone();
494 push_equivalences
495 .classes
496 .extend(outer_equivalences.classes.clone());
497 for (other, input_equivs) in input_equivalences.iter().enumerate() {
498 if index != other {
499 push_equivalences
500 .classes
501 .extend(input_equivs.classes.clone());
502 }
503 }
504 push_equivalences.project(columns..(columns + child_arity));
505 self.apply(expr, child, push_equivalences, get_equivalences, ctx);
506
507 columns += child_arity;
508 }
509
510 equivalences.clone_from(&join_equivalences.classes);
511 }
512 MirRelationExpr::Reduce {
513 input,
514 group_key,
515 aggregates,
516 ..
517 } => {
518 // TODO: MIN, MAX, ANY, ALL aggregates pass through all certain properties of their columns.
519 // This may involve projection and permutation, to reposition the information appropriately.
520 // TODO: Non-null constraints likely push down into the support of the aggregate expressions.
521
522 // Apply any equivalences about the input to key and aggregate expressions.
523 let input_equivalences = derived
524 .last_child()
525 .value::<Equivalences>()
526 .expect("Equivalences required");
527 if let Some(input_equivalences) = input_equivalences {
528 let input_type = derived
529 .last_child()
530 .value::<RelationType>()
531 .expect("RelationType required");
532 let reducer = input_equivalences.reducer();
533 for key in group_key.iter_mut() {
534 // Semijoin elimination currently fails if you do more advanced simplification than
535 // literal substitution.
536 let old_key = key.clone();
537 let changed = reducer.reduce_expr(key);
538 let acceptable_sub = literal_domination(&old_key, key);
539 if changed || !ctx.features.enable_less_reduce_in_eqprop {
540 key.reduce(input_type.as_ref().unwrap());
541 }
542 if !acceptable_sub && !literal_domination(&old_key, key) {
543 key.clone_from(&old_key);
544 }
545 }
546 for aggr in aggregates.iter_mut() {
547 let changed = reducer.reduce_expr(&mut aggr.expr);
548 if changed || !ctx.features.enable_less_reduce_in_eqprop {
549 aggr.expr.reduce(input_type.as_ref().unwrap());
550 }
551 // A count expression over a non-null expression can discard the expression.
552 if aggr.func == mz_expr::AggregateFunc::Count && !aggr.distinct {
553 let mut probe = aggr.expr.clone().call_is_null();
554 reducer.reduce_expr(&mut probe);
555 if probe.is_literal_false() {
556 aggr.expr = MirScalarExpr::literal_true();
557 }
558 }
559 }
560 }
561
562 // To transform `outer_equivalences` to one about `input`, we will "pretend" to pre-pend all of
563 // the input columns, introduce equivalences about the evaluation of `group_key` on them
564 // and the key columns themselves, and then project onto these "input" columns.
565 let input_arity = *derived
566 .last_child()
567 .value::<Arity>()
568 .expect("Arity required");
569 let output_arity = *derived.value::<Arity>().expect("Arity required");
570
571 // Permute `outer_equivalences` to reference columns `input_arity` later.
572 let permutation = (input_arity..(input_arity + output_arity)).collect::<Vec<_>>();
573 outer_equivalences.permute(&permutation[..]);
574 for (index, group) in group_key.iter().enumerate() {
575 outer_equivalences.classes.push(vec![
576 MirScalarExpr::Column(input_arity + index),
577 group.clone(),
578 ]);
579 }
580 outer_equivalences.project(0..input_arity);
581 self.apply(
582 input,
583 derived.last_child(),
584 outer_equivalences,
585 get_equivalences,
586 ctx,
587 );
588 }
589 MirRelationExpr::TopK {
590 input,
591 group_key,
592 limit,
593 ..
594 } => {
595 // We must be careful when updating `limit` to not install column references
596 // outside of `group_key`. We'll do this for now with `literal_domination`,
597 // which will ensure we only perform substitutions by a literal.
598 let input_equivalences = derived
599 .last_child()
600 .value::<Equivalences>()
601 .expect("Equivalences required");
602 if let Some(input_equivalences) = input_equivalences {
603 let input_types = derived
604 .last_child()
605 .value::<RelationType>()
606 .expect("RelationType required");
607 let reducer = input_equivalences.reducer();
608 if let Some(expr) = limit {
609 let old_expr = expr.clone();
610 let changed = reducer.reduce_expr(expr);
611 let acceptable_sub = literal_domination(&old_expr, expr);
612 if changed || !ctx.features.enable_less_reduce_in_eqprop {
613 expr.reduce(input_types.as_ref().unwrap());
614 }
615 if !acceptable_sub && !literal_domination(&old_expr, expr) {
616 expr.clone_from(&old_expr);
617 }
618 }
619 }
620
621 // Discard equivalences among non-key columns, as it is not correct that `input` may drop rows
622 // that violate constraints among non-key columns without affecting the result.
623 outer_equivalences.project(0..group_key.len());
624 self.apply(
625 input,
626 derived.last_child(),
627 outer_equivalences,
628 get_equivalences,
629 ctx,
630 );
631 }
632 MirRelationExpr::Negate { input } => {
633 self.apply(
634 input,
635 derived.last_child(),
636 outer_equivalences,
637 get_equivalences,
638 ctx,
639 );
640 }
641 MirRelationExpr::Threshold { input } => {
642 self.apply(
643 input,
644 derived.last_child(),
645 outer_equivalences,
646 get_equivalences,
647 ctx,
648 );
649 }
650 MirRelationExpr::Union { .. } => {
651 for (child, derived) in expr.children_mut().rev().zip(derived.children_rev()) {
652 self.apply(
653 child,
654 derived,
655 outer_equivalences.clone(),
656 get_equivalences,
657 ctx,
658 );
659 }
660 }
661 MirRelationExpr::ArrangeBy { input, .. } => {
662 // TODO: Option to alter arrangement keys, though .. terrifying.
663 self.apply(
664 input,
665 derived.last_child(),
666 outer_equivalences,
667 get_equivalences,
668 ctx,
669 );
670 }
671 }
672 }
673}
674
675/// Logic encapsulating our willingness to accept an expression simplification.
676///
677/// For reasons of robustness, we cannot yet perform all recommended simplifications.
678/// Certain transforms expect idiomatic expressions, often around precise use of column
679/// identifiers, rather than equivalent identifiers.
680///
681/// The substitutions we are confident with are those that introduce literals for columns,
682/// or which replace column nullability checks with literals.
683fn literal_domination(old: &MirScalarExpr, new: &MirScalarExpr) -> bool {
684 let mut todo = vec![(old, new)];
685 while let Some((old, new)) = todo.pop() {
686 match (old, new) {
687 (_, MirScalarExpr::Literal(_, _)) => {
688 // Substituting a literal is always acceptable; we don't need to consult
689 // the result of the old expression to determine this.
690 }
691 (
692 MirScalarExpr::CallUnary { func: f0, expr: e0 },
693 MirScalarExpr::CallUnary { func: f1, expr: e1 },
694 ) => {
695 if f0 != f1 {
696 return false;
697 } else {
698 todo.push((&**e0, &**e1));
699 }
700 }
701 (
702 MirScalarExpr::CallBinary {
703 func: f0,
704 expr1: e01,
705 expr2: e02,
706 },
707 MirScalarExpr::CallBinary {
708 func: f1,
709 expr1: e11,
710 expr2: e12,
711 },
712 ) => {
713 if f0 != f1 {
714 return false;
715 } else {
716 todo.push((&**e01, &**e11));
717 todo.push((&**e02, &**e12));
718 }
719 }
720 (
721 MirScalarExpr::CallVariadic {
722 func: f0,
723 exprs: e0s,
724 },
725 MirScalarExpr::CallVariadic {
726 func: f1,
727 exprs: e1s,
728 },
729 ) => {
730 use itertools::Itertools;
731 if f0 != f1 {
732 return false;
733 } else {
734 todo.extend(e0s.iter().zip_eq(e1s));
735 }
736 }
737 (
738 MirScalarExpr::If {
739 cond: c0,
740 then: t0,
741 els: e0,
742 },
743 MirScalarExpr::If {
744 cond: c1,
745 then: t1,
746 els: e1,
747 },
748 ) => {
749 todo.push((&**c0, &**c1));
750 todo.push((&**t0, &**t1));
751 todo.push((&**e0, &**e1))
752 }
753 _ => {
754 if old != new {
755 return false;
756 }
757 }
758 }
759 }
760 true
761}