@samitouri / QOS-React-2 / commits / 1d5d89b8be

[js/rust] Improve constant propagation

Makes the same improvement to constant propagation in both the JS and Rust versions. The core algorithm only populates phi variables if all operands have a known value (no back edges) and all those values are the same: this allows us to propagate constants in most cases and simply punts on handling propagating values that are affected by loops. However, since we collapse if statements into gotos when the test condition is a constant, there can be cases where a phi that originally existed will be pruned out: ``` // bb0 let x1; if (true) { // bb1 x2 = 1; } else { // bb2 x3 = 2; // this block becomes unreachable } // bb3 x4 = phi(bb1: x2, bb2: x3); // this phi will get pruned s.t. x4 = x2 = 1 return x4; ``` However, the algorithm doesn't prune phis until _after_ applying constants, so currently we would see this phi node w different inputs and not propagate a constant for the final usage of x, even though it will clearly be `1`. The change is to make constant propagation use fixpoint iteration, iterating so long as terminals changed on the previous iteration. If no terminals change the algorithm completes in a single pass, but if terminals do change then we update phis and continue. As you can see from the new test case this allows us to find arbitrary length sequences of values and terminals that can be pruned.

Joe Savona committed Jul 13, 2023 at 09:53 UTC 1d5d89b8be7647b55c71dd670338a1af184bd3e4
9 files changed +209 -54
compiler/forget/crates/fixtures/tests/fixtures/constant-propagation-constant-if-condition.js
+31 -14
@@ -1,17 +1,34 @@
1 -function Component(props) {
2 - let x = true;
3 - let y;
4 - if (x) {
5 - y = 42;
1 +function Component() {
2 + let a = 1;
3 +
4 + let b;
5 + if (a === 1) {
6 + b = true;
7 } else {
7 - y = "nope";
8 + b = false;
9 }
9 - // TODO: constant propagate the value of `y` here. we can track which
10 - // blocks are reachable as we proceed through, and account for phi
11 - // operands for blocks that weren't reached.
12 - // something like: track a set of reachable blocks, which populate from
13 - // successors of previous block's terminals. but when we see an if w a
14 - // constant test value, we only populate as reachable the corresponding
15 - // branch's block.
16 - return y;
10 +
11 + let c;
12 + if (b) {
13 + c = "hello";
14 + } else {
15 + c = null;
16 + }
17 +
18 + let d;
19 + if (c === "hello") {
20 + d = 42.0;
21 + } else {
22 + d = 42.001;
23 + }
24 +
25 + let e;
26 + if (d === 42.0) {
27 + e = "ok";
28 + } else {
29 + e = "nope";
30 + }
31 +
32 + // should constant-propagate to "ok"
33 + return e;
34 }
compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@constant-propagation-constant-if-condition.js.snap
+77 -18
@@ -4,36 +4,95 @@ expression: "format!(\"Input:\\n{input}\\n\\nOutput:\\n{output}\")"
4 input_file: crates/fixtures/tests/fixtures/constant-propagation-constant-if-condition.js
5 ---
6 Input:
7 -function Component(props) {
8 - let x = true;
9 - let y;
10 - if (x) {
11 - y = 42;
7 +function Component() {
8 + let a = 1;
9 +
10 + let b;
11 + if (a === 1) {
12 + b = true;
13 + } else {
14 + b = false;
15 + }
16 +
17 + let c;
18 + if (b) {
19 + c = "hello";
20 } else {
13 - y = "nope";
21 + c = null;
22 }
15 - return y;
23 +
24 + let d;
25 + if (c === "hello") {
26 + d = 42.0;
27 + } else {
28 + d = 42.001;
29 + }
30 +
31 + let e;
32 + if (d === 42.0) {
33 + e = "ok";
34 + } else {
35 + e = "nope";
36 + }
37 +
38 + // should constant-propagate to "ok"
39 + return e;
40 }
41
42
43 Output:
44 function Component(
21 - unknown props$3,
45 )
46 entry bb0
47 bb0 (block)
25 - [0] #0 = true
26 - [1] #1 = StoreLocal Let unknown x$4 = unknown #0
27 - [2] #2 = DeclareLocal Let unknown y$5
28 - [3] #7 = true
29 - [4] Goto bb2
48 + [0] #0 = 1
49 + [1] #1 = StoreLocal Let unknown a$5 = unknown #0
50 + [2] #2 = DeclareLocal Let unknown b$6
51 + [3] #7 = 1
52 + [4] #8 = 1
53 + [5] #9 = true
54 + [6] Goto bb2
55 bb2 (block)
56 predecessors: bb0
32 - [5] #3 = 42
33 - [6] #4 = StoreLocal Reassign unknown y$6 = unknown #3
34 - [7] Goto bb1
57 + [7] #3 = true
58 + [8] #4 = StoreLocal Reassign unknown b$7 = unknown #3
59 + [9] Goto bb1
60 bb1 (block)
61 predecessors: bb2
37 - [8] #8 = LoadLocal unknown y$6
38 - [9] Return unknown #8
62 + [10] #10 = DeclareLocal Let unknown c$9
63 + [11] #15 = true
64 + [12] Goto bb5
65 +bb5 (block)
66 + predecessors: bb1
67 + [13] #11 = "hello"
68 + [14] #12 = StoreLocal Reassign unknown c$11 = unknown #11
69 + [15] Goto bb4
70 +bb4 (block)
71 + predecessors: bb5
72 + [16] #16 = DeclareLocal Let unknown d$13
73 + [17] #21 = "hello"
74 + [18] #22 = "hello"
75 + [19] #23 = true
76 + [20] Goto bb8
77 +bb8 (block)
78 + predecessors: bb4
79 + [21] #17 = 42
80 + [22] #18 = StoreLocal Reassign unknown d$15 = unknown #17
81 + [23] Goto bb7
82 +bb7 (block)
83 + predecessors: bb8
84 + [24] #24 = DeclareLocal Let unknown e$17
85 + [25] #29 = 42
86 + [26] #30 = 42
87 + [27] #31 = true
88 + [28] Goto bb11
89 +bb11 (block)
90 + predecessors: bb7
91 + [29] #25 = "ok"
92 + [30] #26 = StoreLocal Reassign unknown e$19 = unknown #25
93 + [31] Goto bb10
94 +bb10 (block)
95 + predecessors: bb11
96 + [32] #32 = "ok"
97 + [33] Return unknown #32
98
compiler/forget/crates/hir-optimization/src/constant_propagation.rs
+9 -8
@@ -10,8 +10,11 @@ use hir_ssa::eliminate_redundant_phis;
10
11 pub fn constant_propagation<'a>(env: &Environment<'a>, fun: &mut Function<'a>) {
12 let mut constants = Constants::new();
13 - let have_terminals_changed = apply_constant_propagation(env, fun, &mut constants);
14 - if have_terminals_changed {
13 + loop {
14 + let have_terminals_changed = apply_constant_propagation(env, fun, &mut constants);
15 + if !have_terminals_changed {
16 + break;
17 + }
18 // If terminals have changed then blocks may have become newly unreachable,
19 // so reinitialize the HIR
20 // TODO handle errors
@@ -84,11 +87,9 @@ fn apply_constant_propagation<'a>(
87 fun.body.instructions[instr_ix].value = instr;
88 }
89
87 - if block.kind != BlockKind::Block {
88 - // can't rewrite terminals in value blocks yet
89 - continue;
90 - }
91 -
90 + // If the block ends in an `if` and the test value is a constant primitive,
91 + // then convert the terminal into a goto to either the consequent or alternate
92 + // in this case, only the selected branch is reachable
93 if let TerminalValue::If(terminal) = &mut block.terminal.value {
94 if let Some(primitive) =
95 read_primitive_instruction(&fun.body.instructions, &terminal.test)
@@ -168,7 +169,7 @@ fn evaluate_instruction<'a>(
169 }
170
171 fn apply_binary_operator<'a>(
171 - env: &Environment<'a>,
172 + _env: &Environment<'a>,
173 left: Primitive<'a>,
174 operator: BinaryOperator,
175 right: Primitive<'a>,
compiler/forget/packages/babel-plugin-react-forget/src/Optimization/ConstantPropagation.ts
+5 -6
@@ -53,8 +53,11 @@ export function constantPropagation(fn: HIRFunction): void {
53 }
54
55 function constantPropagationImpl(fn: HIRFunction, constants: Constants): void {
56 - const haveTerminalsChanged = applyConstantPropagation(fn, constants);
57 - if (haveTerminalsChanged) {
56 + while (true) {
57 + const haveTerminalsChanged = applyConstantPropagation(fn, constants);
58 + if (!haveTerminalsChanged) {
59 + break;
60 + }
61 // If terminals have changed then blocks may have become newly unreachable.
62 // Re-run minification of the graph (incl reordering instruction ids)
63 reversePostorderBlocks(fn.body);
@@ -156,10 +159,6 @@ function applyConstantPropagation(
159 }
160 }
161
159 - if (block.kind !== "block") {
160 - // can't rewrite terminals in value blocks yet
161 - continue;
162 - }
162 const terminal = block.terminal;
163 switch (terminal.kind) {
164 case "if": {
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/sequentially-constant-progagatable-if-test-conditions.expect.md new
+50
@@ -0,0 +1,50 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +function Component() {
6 + let a = 1;
7 +
8 + let b;
9 + if (a === 1) {
10 + b = true;
11 + } else {
12 + b = false;
13 + }
14 +
15 + let c;
16 + if (b) {
17 + c = "hello";
18 + } else {
19 + c = null;
20 + }
21 +
22 + let d;
23 + if (c === "hello") {
24 + d = 42.0;
25 + } else {
26 + d = 42.001;
27 + }
28 +
29 + let e;
30 + if (d === 42.0) {
31 + e = "ok";
32 + } else {
33 + e = "nope";
34 + }
35 +
36 + // should constant-propagate to "ok"
37 + return e;
38 +}
39 +
40 +```
41 +
42 +## Code
43 +
44 +```javascript
45 +function Component() {
46 + return "ok";
47 +}
48 +
49 +```
50 +
\ No newline at end of file
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/sequentially-constant-progagatable-if-test-conditions.js new
+34
@@ -0,0 +1,34 @@
1 +function Component() {
2 + let a = 1;
3 +
4 + let b;
5 + if (a === 1) {
6 + b = true;
7 + } else {
8 + b = false;
9 + }
10 +
11 + let c;
12 + if (b) {
13 + c = "hello";
14 + } else {
15 + c = null;
16 + }
17 +
18 + let d;
19 + if (c === "hello") {
20 + d = 42.0;
21 + } else {
22 + d = 42.001;
23 + }
24 +
25 + let e;
26 + if (d === 42.0) {
27 + e = "ok";
28 + } else {
29 + e = "nope";
30 + }
31 +
32 + // should constant-propagate to "ok"
33 + return e;
34 +}
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-objectexpression-phi.expect.md
+1 -4
@@ -24,12 +24,9 @@ function foo() {
24 import { unstable_useMemoCache as useMemoCache } from "react";
25 function foo() {
26 const $ = useMemoCache(1);
27 - const x = 1;
28 -
29 - const y = 3;
27 let t0;
28 if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
32 - t0 = { x, y };
29 + t0 = { x: 1, y: 3 };
30 $[0] = t0;
31 } else {
32 t0 = $[0];
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-return.expect.md
+1 -2
@@ -17,8 +17,7 @@ function foo() {
17
18 ```javascript
19 function foo() {
20 - const x = 2;
21 - return x;
20 + return 2;
21 }
22
23 ```
compiler/forget/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/ssa-throw.expect.md
+1 -2
@@ -16,8 +16,7 @@ function foo() {
16
17 ```javascript
18 function foo() {
19 - const x = 2;
20 - throw x;
19 + throw 2;
20 }
21
22 ```