Skip to main content

rucc_opt/
simplify_cfg.rs

1//! Control flow simplification: a branch whose condition is already known becomes a jump, and
2//! the blocks that leaves stranded are removed.
3//!
4//! Design: section 6.5 of `spec/optimizer/06-cfg-and-dominators.md`, which states the rule for the
5//! whole optimizer, that a block the entry does not reach is invisible to every analysis and is
6//! deleted here rather than by whichever pass happened to notice it.
7//!
8//! # Why this is not only an optimization
9//!
10//! Issue 359 is a program that does not link:
11//!
12//! ```c
13//! extern void link_error(void);
14//! void foo(int x) {
15//!     switch (x) {
16//!     case 0:
17//!         if (0) { link_error(); case 1: bar(); }
18//!     }
19//! }
20//! ```
21//!
22//! Nothing calls `link_error`, so a compiler that emits the call produces an object file that
23//! does not link, and the difference between the two compilers is not how fast the program runs.
24//! The file is in a suite of forty years of compiler bugs for the reason the `case 1:` is where
25//! it is: control does reach `bar` through the switch, and it reaches it from inside the body of
26//! the dead `if`. A compiler that deletes the compound statement gets this as wrong as one that
27//! keeps all of it.
28//!
29//! Doing it in two steps is what makes that come out right without a special case for it. The
30//! branch on the constant becomes a jump, which takes the edge into the dead arm away, and then
31//! reachability from the entry decides what is left. The block holding `bar` has an edge from the
32//! `switch` and stays. The block holding `link_error` has no edges at all and goes.
33//!
34//! # The condition it can read
35//!
36//! A constant, and a comparison of two constants. The second is here rather than in
37//! [`crate::fold`] because folding a comparison would produce an `i1` standing on its own, which
38//! is issue 352 and does not lower, so the pass that folds arithmetic deliberately leaves
39//! comparisons alone. Reading one to decide which way a branch goes produces no `i1` at all: the
40//! comparison is left exactly where it was, used by nothing, and [`crate::dce`] takes it out.
41//!
42//! # Fuel
43//!
44//! Fuel is charged for each branch that folds and not for the blocks that go with it. The
45//! removal is the second half of the transformation that was already paid for rather than a
46//! transformation of its own, and a fuel limit that could stop between the two halves would hand
47//! the verifier a block nothing reaches. Section 41.5 of `spec/optimizer/41-correctness.md` asks
48//! for fuel that is monotonic, which means each step being all of one change and not part of one.
49
50use rucc_ir::{Block, BlockCall, Def, Extra, Func, Inst, IntPred, Opcode, Value};
51
52use crate::fold::constant;
53use crate::{Analyses, Fuel, Pass, Preserved, Stats};
54
55/// Recorded once for each branch that turned into a jump.
56const FOLDED: &str = "branch on a condition that is always the same way replaced by a jump";
57
58/// Recorded once for each block that went with it.
59const REMOVED: &str = "block nothing reaches removed";
60
61/// Recorded for a branch that would have folded if there had been fuel for it.
62const NO_FUEL: &str = "branch on a known condition left alone, the pass ran out of fuel";
63
64/// The pass.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct SimplifyCfg;
67
68impl Pass for SimplifyCfg {
69    fn name(&self) -> &'static str {
70        "simplify-cfg"
71    }
72
73    fn describe(&self) -> &'static str {
74        "a branch whose condition is known becomes a jump, and unreachable blocks are removed"
75    }
76
77    fn preserves(&self) -> Preserved {
78        // Nothing at all, and this is the pass the declaration exists for. An edge moves, so the
79        // graph is a different graph, and everything built on the graph was about the old one.
80        Preserved::NONE
81    }
82
83    fn run(&self, func: &mut Func, an: &mut Analyses, fuel: &mut Fuel) -> Stats {
84        let mut stats = Stats::new();
85        for block in func.blocks().collect::<Vec<Block>>() {
86            let Some(term) = func.terminator(block) else { continue };
87            let Some(taken) = taken(func, term) else { continue };
88            if !fuel.take() {
89                // Out of fuel stops the transforming and not the looking, the same way the other
90                // passes treat it, so that the walk is the same walk at every fuel setting.
91                stats.missed(NO_FUEL);
92                continue;
93            }
94            jump_to(func, term, taken);
95            stats.optimized(FOLDED);
96        }
97        if !stats.changed() {
98            // Nothing moved, so nothing can have been stranded. The graph is not computed at
99            // all here, which is what keeps this pass free on the functions that have no branch
100            // it can read, which is most of them.
101            return stats;
102        }
103        // The cache is holding answers about the function as it was a moment ago. The manager
104        // clears it after the pass returns, which is too late for the pass itself.
105        an.clear();
106        for block in stranded(func, an) {
107            func.remove_block(block);
108            stats.optimized(REMOVED);
109        }
110        stats
111    }
112}
113
114/// Where this terminator always goes, if it always goes to one place.
115///
116/// `None` is every reason not to fold and does not say which, because the answer to all of them
117/// is to leave the branch alone.
118fn taken(func: &Func, term: Inst) -> Option<BlockCall> {
119    let data = &func[term];
120    let arg = *func[data.args].first()?;
121    match data.opcode {
122        Opcode::BrIf => {
123            let Extra::Targets(targets) = data.extra else { return None };
124            // The first target is the one taken when the condition is one, which is what
125            // `Builder::br_if` writes and what the printer reads back.
126            let arm = usize::from(!known(func, arg)?);
127            func[targets].get(arm).copied()
128        }
129        Opcode::Switch => {
130            let Extra::Switch(at) = data.extra else { return None };
131            let (value, _) = constant(func, arg)?;
132            let info = func[at];
133            // The default is the first target and the cases follow it in the order their values
134            // are in, so the target for a case that matches is one past the value's own place.
135            let case = func[info.cases].iter().position(|it| *it == value);
136            func[info.targets].get(case.map_or(0, |case| case + 1)).copied()
137        }
138        _ => None,
139    }
140}
141
142/// Rewrites the terminator as a jump to that one of its targets.
143///
144/// In place, and the target keeps the arguments it already had, because the arguments belong to
145/// the edge and the edge is the one that survives.
146fn jump_to(func: &mut Func, term: Inst, call: BlockCall) {
147    let targets = func.push_block_calls(&[call]);
148    let args = func.push_values(&[]);
149    let data = &mut func[term];
150    data.opcode = Opcode::Jump;
151    data.args = args;
152    data.extra = Extra::Targets(targets);
153}
154
155/// The blocks the entry cannot reach, in block order.
156///
157/// This is reachability as the verifier counts it, which is over the edges the terminators name
158/// and additionally over the blocks a `block_addr` mentions. A block whose address is taken is
159/// arrived at by an `indirect_br` somewhere, and that instruction lists every block the address
160/// can hold, so the edge is already in the graph from the place control really leaves. What the
161/// graph does not carry is the `block_addr` itself, and deleting the block under one would leave
162/// an instruction naming a block that is not there.
163fn stranded(func: &Func, an: &mut Analyses) -> Vec<Block> {
164    let cfg = an.cfg(func);
165    let Some(entry) = cfg.entry() else { return Vec::new() };
166    let mut seen = vec![false; cfg.capacity()];
167    seen[entry.index()] = true;
168    let mut stack = vec![entry];
169    let mut reached = Vec::new();
170    while let Some(block) = stack.pop() {
171        for &succ in cfg.successors(block) {
172            if !seen[succ.index()] {
173                seen[succ.index()] = true;
174                stack.push(succ);
175            }
176        }
177        reached.push(block);
178    }
179    // The addresses in a second walk over the blocks the first one reached, because an address
180    // taken in a block nothing reaches is an address nothing takes.
181    let mut next = reached;
182    while !next.is_empty() {
183        let mut found = Vec::new();
184        for block in next {
185            for inst in func.insts(block) {
186                if func[inst].opcode != Opcode::BlockAddr {
187                    continue;
188                }
189                for call in func.successors(inst) {
190                    if !seen[call.block.index()] {
191                        seen[call.block.index()] = true;
192                        found.push(call.block);
193                    }
194                }
195            }
196        }
197        // Everything the newly kept blocks reach is kept too, which is what makes this a fixed
198        // point rather than one extra step.
199        let mut stack = found.clone();
200        while let Some(block) = stack.pop() {
201            for &succ in cfg.successors(block) {
202                if !seen[succ.index()] {
203                    seen[succ.index()] = true;
204                    stack.push(succ);
205                    found.push(succ);
206                }
207            }
208        }
209        next = found;
210    }
211    func.blocks().filter(|block| !seen[block.index()]).collect()
212}
213
214/// Whether this condition is always true or always false.
215fn known(func: &Func, value: Value) -> Option<bool> {
216    if let Some((imm, _)) = constant(func, value) {
217        return Some(imm.unsigned() != 0);
218    }
219    compared(func, value)
220}
221
222/// What a comparison of two constants comes out as.
223fn compared(func: &Func, value: Value) -> Option<bool> {
224    let Def::Result { inst, .. } = func[value].def else { return None };
225    let data = &func[inst];
226    if data.opcode != Opcode::ICmp {
227        return None;
228    }
229    let Extra::IntPred(pred) = data.extra else { return None };
230    let args = &func[data.args];
231    let (lhs, ty) = constant(func, *args.first()?)?;
232    let (rhs, _) = constant(func, *args.get(1)?)?;
233    Some(match pred {
234        IntPred::Eq => lhs == rhs,
235        IntPred::Ne => lhs != rhs,
236        IntPred::Slt => lhs.signed(ty) < rhs.signed(ty),
237        IntPred::Sle => lhs.signed(ty) <= rhs.signed(ty),
238        IntPred::Sgt => lhs.signed(ty) > rhs.signed(ty),
239        IntPred::Sge => lhs.signed(ty) >= rhs.signed(ty),
240        IntPred::Ult => lhs.unsigned() < rhs.unsigned(),
241        IntPred::Ule => lhs.unsigned() <= rhs.unsigned(),
242        IntPred::Ugt => lhs.unsigned() > rhs.unsigned(),
243        IntPred::Uge => lhs.unsigned() >= rhs.unsigned(),
244    })
245}
246
247#[cfg(test)]
248mod tests {
249    use rucc_base::Interner;
250    use rucc_ir::{Block, Builder, Func, IntPred, Module, Opcode, Signature, Type, Value};
251    use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
252
253    use super::SimplifyCfg;
254    use crate::stats::Kind;
255    use crate::testing::graph;
256    use crate::{Analyses, Fuel, Pass, Preserved, Stats};
257
258    /// Runs the pass with as much fuel as it wants.
259    fn simplify(func: &mut Func) -> Stats {
260        SimplifyCfg.run(func, &mut Analyses::new(), &mut Fuel::unlimited())
261    }
262
263    /// The blocks the function still has, by number.
264    fn blocks(func: &Func) -> Vec<usize> {
265        func.blocks().map(Block::index).collect()
266    }
267
268    /// The opcode of a block's terminator.
269    fn terminator(func: &Func, block: usize) -> Opcode {
270        let block = Block::from_usize(block);
271        func[func.terminator(block).expect("every block here has one")].opcode
272    }
273
274    /// Where a block's terminator goes, as block numbers.
275    fn goes_to(func: &Func, block: usize) -> Vec<usize> {
276        let block = Block::from_usize(block);
277        let term = func.terminator(block).expect("every block here has one");
278        func.successors(term).map(|call| call.block.index()).collect()
279    }
280
281    /// A function with an entry, a `br_if` on `cond`, two arms and a join.
282    ///
283    /// The condition is built by the caller out of the builder it is handed, which is what lets
284    /// one shape stand for a constant, a comparison and a value nothing knows anything about.
285    fn diamond(cond: impl FnOnce(&mut Builder<'_>) -> Value) -> Func {
286        let mut names = Interner::new();
287        let mut func = Func::new(names.intern("f"), Signature::new());
288        let entry = func.create_block();
289        let then_block = func.create_block();
290        let else_block = func.create_block();
291        let join = func.create_block();
292        let mut build = Builder::new(&mut func, entry);
293        let cond = cond(&mut build);
294        build.br_if(cond, then_block, &[], else_block, &[]);
295        for arm in [then_block, else_block] {
296            let mut build = Builder::new(&mut func, arm);
297            build.jump(join, &[]);
298        }
299        let mut build = Builder::new(&mut func, join);
300        build.ret(&[]);
301        func
302    }
303
304    #[test]
305    fn a_branch_on_a_true_constant_becomes_a_jump_to_the_first_arm() {
306        let mut func = diamond(|build| build.iconst(Type::int(1), 1));
307        let stats = simplify(&mut func);
308        assert!(stats.changed());
309        assert_eq!(terminator(&func, 0), Opcode::Jump);
310        assert_eq!(goes_to(&func, 0), [1]);
311        // And the arm it did not take is gone, because nothing else went there.
312        assert_eq!(blocks(&func), [0, 1, 3]);
313        assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 1);
314    }
315
316    #[test]
317    fn a_branch_on_a_false_constant_becomes_a_jump_to_the_second_arm() {
318        let mut func = diamond(|build| build.iconst(Type::int(1), 0));
319        assert!(simplify(&mut func).changed());
320        assert_eq!(goes_to(&func, 0), [2]);
321        assert_eq!(blocks(&func), [0, 2, 3]);
322    }
323
324    #[test]
325    fn a_branch_on_a_comparison_of_two_constants_is_read_without_folding_it() {
326        // Both ways round on every predicate, which is where a sign error or an inverted
327        // comparison would hide. A comparison the pass reads is left standing, because folding
328        // it would produce an `i1` on its own and issue 352 says that does not lower.
329        let cases: &[(IntPred, i128, i128, bool)] = &[
330            (IntPred::Eq, 7, 7, true),
331            (IntPred::Eq, 7, 8, false),
332            (IntPred::Ne, 7, 8, true),
333            (IntPred::Ne, 7, 7, false),
334            (IntPred::Slt, -1, 1, true),
335            (IntPred::Slt, 1, -1, false),
336            (IntPred::Sle, -1, -1, true),
337            (IntPred::Sle, 1, -1, false),
338            (IntPred::Sgt, 1, -1, true),
339            (IntPred::Sgt, -1, 1, false),
340            (IntPred::Sge, -1, -1, true),
341            (IntPred::Sge, -1, 1, false),
342            (IntPred::Ult, 1, -1, true),
343            (IntPred::Ult, -1, 1, false),
344            (IntPred::Ule, -1, -1, true),
345            (IntPred::Ule, -1, 1, false),
346            (IntPred::Ugt, -1, 1, true),
347            (IntPred::Ugt, 1, -1, false),
348            (IntPred::Uge, -1, -1, true),
349            (IntPred::Uge, 1, -1, false),
350        ];
351        for &(pred, lhs, rhs, taken) in cases {
352            let mut func = diamond(|build| {
353                let lhs = build.iconst(Type::int(32), lhs);
354                let rhs = build.iconst(Type::int(32), rhs);
355                build.icmp(pred, lhs, rhs)
356            });
357            assert!(simplify(&mut func).changed(), "{pred:?} {lhs} {rhs}");
358            let arm = if taken { 1 } else { 2 };
359            assert_eq!(goes_to(&func, 0), [arm], "{pred:?} {lhs} {rhs}");
360            let kept = func.insts(Block::from_usize(0)).any(|it| func[it].opcode == Opcode::ICmp);
361            assert!(kept, "the comparison was folded away and issue 352 says it must not be");
362        }
363    }
364
365    #[test]
366    fn a_branch_on_something_nobody_knows_is_left_alone() {
367        let mut names = Interner::new();
368        let mut func = Func::new(names.intern("f"), Signature::new().with_params(&[Type::int(1)]));
369        let entry = func.create_block();
370        let then_block = func.create_block();
371        let else_block = func.create_block();
372        let cond = func.append_param(entry, Type::int(1));
373        let mut build = Builder::new(&mut func, entry);
374        build.br_if(cond, then_block, &[], else_block, &[]);
375        for arm in [then_block, else_block] {
376            let mut build = Builder::new(&mut func, arm);
377            build.ret(&[]);
378        }
379        let stats = simplify(&mut func);
380        assert!(!stats.changed());
381        assert!(stats.is_empty(), "a pass with nothing to say should say nothing");
382        assert_eq!(terminator(&func, 0), Opcode::BrIf);
383        assert_eq!(blocks(&func), [0, 1, 2]);
384    }
385
386    #[test]
387    fn a_switch_on_a_constant_takes_the_case_that_matches() {
388        let mut names = Interner::new();
389        let mut func = Func::new(names.intern("f"), Signature::new());
390        let entry = func.create_block();
391        let default = func.create_block();
392        let first = func.create_block();
393        let second = func.create_block();
394        let mut build = Builder::new(&mut func, entry);
395        let value = build.iconst(Type::int(32), 5);
396        build.switch(value, default, &[(4, first), (5, second)]);
397        for arm in [default, first, second] {
398            let mut build = Builder::new(&mut func, arm);
399            build.ret(&[]);
400        }
401        assert!(simplify(&mut func).changed());
402        assert_eq!(terminator(&func, 0), Opcode::Jump);
403        assert_eq!(goes_to(&func, 0), [3]);
404        assert_eq!(blocks(&func), [0, 3]);
405    }
406
407    #[test]
408    fn a_switch_on_a_constant_no_case_names_takes_the_default() {
409        let mut names = Interner::new();
410        let mut func = Func::new(names.intern("f"), Signature::new());
411        let entry = func.create_block();
412        let default = func.create_block();
413        let case = func.create_block();
414        let mut build = Builder::new(&mut func, entry);
415        let value = build.iconst(Type::int(32), 9);
416        build.switch(value, default, &[(4, case)]);
417        for arm in [default, case] {
418            let mut build = Builder::new(&mut func, arm);
419            build.ret(&[]);
420        }
421        assert!(simplify(&mut func).changed());
422        assert_eq!(goes_to(&func, 0), [1]);
423        assert_eq!(blocks(&func), [0, 1]);
424    }
425
426    #[test]
427    fn the_arguments_travel_with_the_edge_that_survives() {
428        // The whole reason there are no phi nodes: the argument is in the branch beside the
429        // block it goes to, so the surviving arm brings its own and the other one leaves with
430        // the edge it was on.
431        let mut names = Interner::new();
432        let mut func = Func::new(names.intern("f"), Signature::new());
433        let entry = func.create_block();
434        let join = func.create_block();
435        let param = func.append_param(join, Type::int(32));
436        let mut build = Builder::new(&mut func, entry);
437        let cond = build.iconst(Type::int(1), 0);
438        let taken = build.iconst(Type::int(32), 11);
439        let other = build.iconst(Type::int(32), 22);
440        build.br_if(cond, join, &[other], join, &[taken]);
441        let mut build = Builder::new(&mut func, join);
442        build.ret(&[]);
443        assert!(simplify(&mut func).changed());
444        let term = func.terminator(entry).expect("the entry has one");
445        let call = func.successors(term).next().expect("a jump goes somewhere");
446        assert_eq!(func[call.args], [taken]);
447        assert_eq!(func[join].params, [param]);
448    }
449
450    #[test]
451    fn a_block_the_dead_arm_shared_with_a_live_one_stays() {
452        // Issue 359 in the small. The block holding `bar` is inside the body of the dead `if`
453        // and is a `case` of the switch as well, so the arm goes and the block does not.
454        let mut names = Interner::new();
455        let mut func = Func::new(names.intern("f"), Signature::new().with_params(&[Type::int(32)]));
456        let entry = func.create_block();
457        let dead = func.create_block();
458        let shared = func.create_block();
459        let exit = func.create_block();
460        let x = func.append_param(entry, Type::int(32));
461        let mut build = Builder::new(&mut func, entry);
462        let never = build.iconst(Type::int(1), 0);
463        build.switch(x, exit, &[(0, dead), (1, shared)]);
464        // The `if (0)` inside the first case, whose body is where the second case's label sits.
465        let mut build = Builder::new(&mut func, dead);
466        build.br_if(never, shared, &[], exit, &[]);
467        for arm in [shared, exit] {
468            let mut build = Builder::new(&mut func, arm);
469            build.ret(&[]);
470        }
471        let stats = simplify(&mut func);
472        assert!(stats.changed());
473        // The switch is on a parameter, so it stays. The branch inside the dead arm folds to the
474        // exit, and nothing is removed at all, because the shared block is still a case.
475        assert_eq!(terminator(&func, 0), Opcode::Switch);
476        assert_eq!(goes_to(&func, 1), [3]);
477        assert_eq!(blocks(&func), [0, 1, 2, 3]);
478        assert_eq!(stats.count(Kind::Optimized, super::REMOVED), 0);
479    }
480
481    #[test]
482    fn a_block_whose_address_is_taken_is_not_removed() {
483        // Reachability here has to be the verifier's reachability. The graph does not carry the
484        // edge from a `block_addr` to the block it names, and a pass that removed the block
485        // under one would leave an instruction pointing at nothing.
486        let mut names = Interner::new();
487        let mut func = Func::new(names.intern("f"), Signature::new());
488        let entry = func.create_block();
489        let labelled = func.create_block();
490        let arm = func.create_block();
491        let mut build = Builder::new(&mut func, entry);
492        let cond = build.iconst(Type::int(1), 1);
493        let addr = build.block_addr(labelled);
494        build.br_if(cond, arm, &[], labelled, &[]);
495        let mut build = Builder::new(&mut func, arm);
496        build.indirect_br(addr, &[labelled]);
497        let mut build = Builder::new(&mut func, labelled);
498        build.ret(&[]);
499        assert!(simplify(&mut func).changed());
500        assert_eq!(goes_to(&func, 0), [2]);
501        assert!(blocks(&func).contains(&1), "the labelled block went with the arm");
502    }
503
504    #[test]
505    fn a_block_only_an_unreachable_block_takes_the_address_of_goes_too() {
506        // The other half of the same rule. Once the block holding the `block_addr` is gone, the
507        // address is gone with it, and the block it named is reached by nothing.
508        let mut names = Interner::new();
509        let mut func = Func::new(names.intern("f"), Signature::new());
510        let entry = func.create_block();
511        let dead = func.create_block();
512        let labelled = func.create_block();
513        let mut build = Builder::new(&mut func, entry);
514        let cond = build.iconst(Type::int(1), 1);
515        build.br_if(cond, entry, &[], dead, &[]);
516        let mut build = Builder::new(&mut func, dead);
517        let addr = build.block_addr(labelled);
518        build.indirect_br(addr, &[labelled]);
519        let mut build = Builder::new(&mut func, labelled);
520        build.ret(&[]);
521        assert!(simplify(&mut func).changed());
522        assert_eq!(blocks(&func), [0]);
523    }
524
525    #[test]
526    fn out_of_fuel_leaves_the_function_exactly_as_it_was() {
527        let mut func = diamond(|build| build.iconst(Type::int(1), 1));
528        let before = blocks(&func);
529        let stats = SimplifyCfg.run(&mut func, &mut Analyses::new(), &mut Fuel::of(0));
530        assert!(!stats.changed());
531        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
532        assert_eq!(terminator(&func, 0), Opcode::BrIf);
533        assert_eq!(blocks(&func), before);
534    }
535
536    #[test]
537    fn what_fuel_buys_is_one_whole_change_and_never_half_of_one() {
538        // Two foldable branches and fuel for one. The half that removes the stranded blocks is
539        // not charged for, because a limit that could stop between the two halves would leave a
540        // block nothing reaches and the verifier would refuse the function.
541        let mut func = graph(&[&[1, 2], &[3, 4], &[5], &[5], &[5], &[]]);
542        let stats = SimplifyCfg.run(&mut func, &mut Analyses::new(), &mut Fuel::of(1));
543        assert_eq!(stats.count(Kind::Optimized, super::FOLDED), 1);
544        assert_eq!(stats.count(Kind::Missed, super::NO_FUEL), 1);
545        // The entry folded to its first arm, so the second arm is stranded and goes, and the
546        // block only it reached goes with it.
547        assert_eq!(blocks(&func), [0, 1, 3, 4, 5]);
548    }
549
550    #[test]
551    fn the_pass_leaves_the_verifier_nothing_to_complain_about() {
552        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
553        let mut names = Interner::new();
554        let mut module = Module::new(names.intern("test.c"), &target);
555        let mut func = graph(&[&[1, 2], &[3], &[3], &[4, 1], &[]]);
556        simplify(&mut func);
557        module.add_func(func);
558        rucc_ir::verify(&module, &names).expect("the pass left the function verifiable");
559    }
560
561    #[test]
562    fn the_pass_says_it_preserves_nothing() {
563        assert_eq!(SimplifyCfg.preserves(), Preserved::NONE);
564    }
565}