@samitouri / QOS-React-2 / commits / 2fb14138d4

[rust] Port EliminateRedundantPhis

This is a nearly 1:1 port of EliminateRedundantPhis to Rust, the algorithm is identical and all differences are superficial. There are few things missing (an invariant instead of a panic in one place, recursing into function expressions) but the Rust version is still going to end up shorter despite keeping all the comments.

Joe Savona committed Jul 12, 2023 at 14:54 UTC 2fb14138d415be3f36797ee2fdfd9146aa1e39a0
10 files changed +237 -14
compiler/forget/Cargo.lock
+10
@@ -504,6 +504,7 @@ dependencies = [
504 "estree",
505 "estree-swc",
506 "hir",
507 + "hir-ssa",
508 "insta",
509 "miette 5.9.0",
510 ]
@@ -644,8 +645,10 @@ version = "0.1.0"
645 dependencies = [
646 "bumpalo",
647 "hir",
648 + "indexmap 2.0.0",
649 "miette 5.9.0",
650 "thiserror",
651 + "utils",
652 ]
653
654 [[package]]
@@ -2782,6 +2785,13 @@ dependencies = [
2785 "percent-encoding",
2786 ]
2787
2788 +[[package]]
2789 +name = "utils"
2790 +version = "0.1.0"
2791 +dependencies = [
2792 + "bumpalo",
2793 +]
2794 +
2795 [[package]]
2796 name = "vergen"
2797 version = "7.5.1"
compiler/forget/Cargo.toml
+1
@@ -9,6 +9,7 @@ members = [
9 "crates/estree",
10 "crates/estree-codegen",
11 "crates/estree-swc",
12 + "crates/utils"
13 ]
14
15 # Make insta run faster by compiling with release mode optimizations
compiler/forget/crates/fixtures/tests/fixtures_test.rs
+3 -2
@@ -1,11 +1,11 @@
1 -use std::fmt::Write;
1 +use std::{env, fmt::Write};
2
3 use build_hir::build;
4 use bumpalo::Bump;
5 use estree::{ModuleItem, Statement};
6 use estree_swc::parse;
7 use hir::{Environment, Print, Registry};
8 -use hir_ssa::enter_ssa;
8 +use hir_ssa::{eliminate_redundant_phis, enter_ssa};
9 use insta::{assert_snapshot, glob};
10 use miette::{NamedSource, Report};
11
@@ -34,6 +34,7 @@ fn fixtures() {
34 match build(&environment, *fun) {
35 Ok(mut fun) => {
36 enter_ssa(&environment, &mut fun).unwrap();
37 + eliminate_redundant_phis(&environment, &mut fun);
38 fun.print(&fun.body, &mut output).unwrap();
39 }
40 Err(error) => {
compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@ssa-reassign-if.js.snap
+2 -5
@@ -54,8 +54,6 @@ bb5 (block)
54 [16] Goto bb3
55 bb3 (block)
56 predecessors: bb4, bb5
57 - x$15: phi(bb4: x$10, bb5: x$10)
58 - y$17: phi(bb4: y$8, bb5: y$8)
57 z$19: phi(bb4: z$11, bb5: z$12)
58 [17] Goto bb1
59 bb6 (block)
@@ -65,11 +63,10 @@ bb6 (block)
63 [20] Goto bb1
64 bb1 (block)
65 predecessors: bb3, bb6
68 - x$14: phi(bb3: x$15, bb6: x$13)
69 - y$16: phi(bb3: y$17, bb6: y$8)
66 + x$14: phi(bb3: x$10, bb6: x$13)
67 z$18: phi(bb3: z$19, bb6: z$9)
68 [21] #15 = LoadLocal unknown x$14
72 - [22] #16 = LoadLocal unknown y$16
69 + [22] #16 = LoadLocal unknown y$8
70 [23] #17 = Binary unknown #15 + unknown #16
71 [24] #18 = LoadLocal unknown z$18
72 [25] #19 = Binary unknown #17 + unknown #18
compiler/forget/crates/hir-ssa/Cargo.toml
+1
@@ -7,6 +7,7 @@ edition = "2021"
7
8 [dependencies]
9 hir = { path = "../hir" }
10 +utils = { path = "../utils" }
11 bumpalo = "3.13.0"
12 indexmap = "2.0.0"
13 miette = { version = "5.9.0" }
compiler/forget/crates/hir-ssa/src/eliminate_redundant_phis.rs new
+96
@@ -0,0 +1,96 @@
1 +use std::collections::{HashMap, HashSet};
2 +
3 +use hir::{BlockId, Environment, Function, Identifier, IdentifierId, HIR};
4 +use utils::RetainMut;
5 +
6 +/// Pass to eliminate redundant phi nodes:
7 +/// all operands are the same identifier, ie `x2 = phi(x1, x1, x1)`.
8 +/// all operands are the same identifier *or* the output of the phi, ie `x2 = phi(x1, x2, x1, x2)`.
9 +///
10 +/// In both these cases, the phi is eliminated and all usages of the phi identifier
11 +/// are replaced with the other operand (ie in both cases above, all usages of `x2` are replaced with `x1` .
12 +///
13 +/// The algorithm is inspired by that in https://pp.info.uni-karlsruhe.de/uploads/publikationen/braun13cc.pdf
14 +/// but modified to reduce passes over the CFG. We visit the blocks in reverse postorder. Each time a redundant
15 +/// phi is encountered we add a mapping (eg x2 -> x1) to a rewrite table. Subsequent instructions, terminals,
16 +/// and phis rewrite all their identifiers based on this table. The algorithm loops over the CFG repeatedly
17 +/// until there are no new rewrites: for a CFG without back-edges it completes in a single pass.
18 +type Rewrites<'a> = HashMap<IdentifierId, Identifier<'a>>;
19 +pub fn eliminate_redundant_phis<'a>(_env: &'a Environment, fun: &mut Function<'a>) {
20 + let hir = &mut fun.body;
21 + let mut rewrites = Rewrites::new();
22 +
23 + let mut has_back_edge = false;
24 + let mut visited = HashSet::<BlockId>::new();
25 +
26 + let mut len;
27 + loop {
28 + len = rewrites.len();
29 +
30 + for (_, block) in hir.blocks.iter_mut() {
31 + if !has_back_edge {
32 + for predecessor in block.predecessors.iter() {
33 + if !visited.contains(predecessor) {
34 + has_back_edge = true;
35 + }
36 + }
37 + }
38 + visited.insert(block.id);
39 +
40 + block.phis.retain_mut(|phi| {
41 + // Remap operands in case they are from eliminated phis
42 + for (_, operand) in phi.operands.iter_mut() {
43 + rewrite(&rewrites, operand);
44 + }
45 + // Find if the phi can be eliminated
46 + let mut rewrite: Option<Identifier> = None;
47 + for (_, operand) in phi.operands.iter() {
48 + if operand.id == phi.identifier.id {
49 + // This operand is the same as the phi itself
50 + continue;
51 + }
52 + match &rewrite {
53 + Some(rewrite) if rewrite.id == operand.id => {
54 + // this operand is the same as the other operands
55 + continue;
56 + }
57 + Some(_) => {
58 + // There are multiple operands not equal to the phi itself,
59 + // the phi cannot be eliminated (true to retain the phi)
60 + return true;
61 + }
62 + None => {
63 + rewrite = Some(operand.clone());
64 + }
65 + }
66 + }
67 + rewrites.insert(phi.identifier.id, rewrite.unwrap());
68 + // The phi can be eliminated (false to not retain)
69 + false
70 + });
71 +
72 + for instr_ix in block.instructions.iter() {
73 + let instr = &mut hir.instructions[usize::from(*instr_ix)];
74 + instr.each_identifier_store(|store| {
75 + rewrite(&rewrites, &mut store.identifier.identifier)
76 + });
77 + instr.each_identifier_load(|load| rewrite(&rewrites, &mut load.identifier));
78 + }
79 + }
80 +
81 + // We only need to loop if there were newly eliminated phis in this iteration
82 + // *and* the CFG has loops. If there are no loops then all eliminated phis must
83 + // have been propagated forwards since we visit in RPO
84 + if has_back_edge && rewrites.len() > len {
85 + continue;
86 + } else {
87 + break;
88 + }
89 + }
90 +}
91 +
92 +fn rewrite<'a>(rewrites: &Rewrites<'a>, identifier: &mut Identifier<'a>) {
93 + if let Some(rewrite) = rewrites.get(&identifier.id) {
94 + *identifier = rewrite.clone()
95 + }
96 +}
compiler/forget/crates/hir-ssa/src/lib.rs
+2 -2
@@ -1,7 +1,7 @@
1 +mod eliminate_redundant_phis;
2 mod enter;
3 mod leave;
3 -mod minimize_phis;
4
5 +pub use eliminate_redundant_phis::eliminate_redundant_phis;
6 pub use enter::enter_ssa;
7 pub use leave::leave_ssa;
7 -pub use minimize_phis::minimize_phis;
compiler/forget/crates/hir-ssa/src/minimize_phis.rs deleted
-5
@@ -1,5 +0,0 @@
1 -use hir::{Environment, HIR};
2 -
3 -pub fn minimize_phis<'a>(_env: &'a Environment, _hir: &mut HIR<'a>) {
4 - todo!("minimize_phis()");
5 -}
compiler/forget/crates/utils/Cargo.toml new
+9
@@ -0,0 +1,9 @@
1 +[package]
2 +name = "utils"
3 +version = "0.1.0"
4 +edition = "2021"
5 +
6 +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 +
8 +[dependencies]
9 +bumpalo = { version = "3.13.0", features = ["boxed", "collections"] }
\ No newline at end of file
compiler/forget/crates/utils/src/lib.rs new
+113
@@ -0,0 +1,113 @@
1 +use bumpalo::collections::Vec;
2 +
3 +pub trait RetainMut<T> {
4 + fn retain_mut<F>(&mut self, f: F) -> ()
5 + where
6 + F: FnMut(&mut T) -> bool;
7 +}
8 +
9 +impl<'a, T> RetainMut<T> for Vec<'a, T> {
10 + fn retain_mut<F>(&mut self, mut f: F) -> ()
11 + where
12 + F: FnMut(&mut T) -> bool,
13 + {
14 + // NOTE: implementation adapted from retain_mut crate
15 + // which is in turn adapted from Rust stdlib
16 + // https://docs.rs/retain_mut/latest/src/retain_mut/lib.rs.html#68-69
17 +
18 + let original_len = self.len();
19 + // Avoid double drop if the drop guard is not executed,
20 + // since we may make some holes during the process.
21 + unsafe { self.set_len(0) };
22 +
23 + // Vec: [Kept, Kept, Hole, Hole, Hole, Hole, Unchecked, Unchecked]
24 + // |<- processed len ->| ^- next to check
25 + // |<- deleted cnt ->|
26 + // |<- original_len ->|
27 + // Kept: Elements which predicate returns true on.
28 + // Hole: Moved or dropped element slot.
29 + // Unchecked: Unchecked valid elements.
30 + //
31 + // This drop guard will be invoked when predicate or `drop` of element panicked.
32 + // It shifts unchecked elements to cover holes and `set_len` to the correct length.
33 + // In cases when predicate and `drop` never panick, it will be optimized out.
34 + struct BackshiftOnDrop<'a, 'b, T> {
35 + v: &'b mut Vec<'a, T>,
36 + processed_len: usize,
37 + deleted_cnt: usize,
38 + original_len: usize,
39 + }
40 +
41 + impl<T> Drop for BackshiftOnDrop<'_, '_, T> {
42 + fn drop(&mut self) {
43 + if self.deleted_cnt > 0 {
44 + // SAFETY: Trailing unchecked items must be valid since we never touch them.
45 + unsafe {
46 + std::ptr::copy(
47 + self.v.as_ptr().add(self.processed_len),
48 + self.v
49 + .as_mut_ptr()
50 + .add(self.processed_len - self.deleted_cnt),
51 + self.original_len - self.processed_len,
52 + );
53 + }
54 + }
55 + // SAFETY: After filling holes, all items are in contiguous memory.
56 + unsafe {
57 + self.v.set_len(self.original_len - self.deleted_cnt);
58 + }
59 + }
60 + }
61 +
62 + let mut g = BackshiftOnDrop {
63 + v: self,
64 + processed_len: 0,
65 + deleted_cnt: 0,
66 + original_len,
67 + };
68 +
69 + fn process_loop<F, T, const DELETED: bool>(
70 + original_len: usize,
71 + f: &mut F,
72 + g: &mut BackshiftOnDrop<'_, '_, T>,
73 + ) where
74 + F: FnMut(&mut T) -> bool,
75 + {
76 + while g.processed_len != original_len {
77 + // SAFETY: Unchecked element must be valid.
78 + let cur = unsafe { &mut *g.v.as_mut_ptr().add(g.processed_len) };
79 + if !f(cur) {
80 + // Advance early to avoid double drop if `drop_in_place` panicked.
81 + g.processed_len += 1;
82 + g.deleted_cnt += 1;
83 + // SAFETY: We never touch this element again after dropped.
84 + unsafe { std::ptr::drop_in_place(cur) };
85 + // We already advanced the counter.
86 + if DELETED {
87 + continue;
88 + } else {
89 + break;
90 + }
91 + }
92 + if DELETED {
93 + // SAFETY: `deleted_cnt` > 0, so the hole slot must not overlap with current element.
94 + // We use copy for move, and never touch this element again.
95 + unsafe {
96 + let hole_slot = g.v.as_mut_ptr().add(g.processed_len - g.deleted_cnt);
97 + std::ptr::copy_nonoverlapping(cur, hole_slot, 1);
98 + }
99 + }
100 + g.processed_len += 1;
101 + }
102 + }
103 +
104 + // Stage 1: Nothing was deleted.
105 + process_loop::<F, T, false>(original_len, &mut f, &mut g);
106 +
107 + // Stage 2: Some elements were deleted.
108 + process_loop::<F, T, true>(original_len, &mut f, &mut g);
109 +
110 + // All item are processed. This can be optimized to `set_len` by LLVM.
111 + drop(g);
112 + }
113 +}