@samitouri / QOS-React-2 / commits / 8ada08f11e

[rust] Port constant propagation

Ports constant propagation to Rust. The algorithm is broadly similar to the TS version, and most of the differences come from the slightly different HIR data model (operands are instruction indices not identifier ids). What this means is that the Constants map that we build up is really only used for variables that existed in the original program, and only comes into play with instructions like LoadLocal and StoreLocal. Other instructions such as Binary just look up their operands directly, ie they load the referenced instruction to check if both left/right are primitives. Note that with SSA form and the index-based operands we could actually get rid of StoreLocal/LoadLocal completely, which would further simplify constant propagation. However: * we'd need to add a Phi instruction kind, not a big deal but it diverges even more * more importantly, it makes it super hard to implement LeaveSSA That second point is a deal-breaker so unless someone has a great idea for how to exit SSA form without having Load/Stores, let's keep them.

Joe Savona committed Jul 12, 2023 at 17:33 UTC 8ada08f11e218f1a33da28a968bfb700c2abc40d
19 files changed +774 -26
compiler/forget/Cargo.lock
+16
@@ -504,6 +504,7 @@ dependencies = [
504 "estree",
505 "estree-swc",
506 "hir",
507 + "hir-optimization",
508 "hir-ssa",
509 "insta",
510 "miette 5.9.0",
@@ -639,6 +640,21 @@ dependencies = [
640 "serde",
641 ]
642
643 +[[package]]
644 +name = "hir-optimization"
645 +version = "0.1.0"
646 +dependencies = [
647 + "build-hir",
648 + "bumpalo",
649 + "estree",
650 + "hir",
651 + "hir-ssa",
652 + "indexmap 2.0.0",
653 + "miette 5.9.0",
654 + "thiserror",
655 + "utils",
656 +]
657 +
658 [[package]]
659 name = "hir-ssa"
660 version = "0.1.0"
compiler/forget/Cargo.toml
+1
@@ -5,6 +5,7 @@ members = [
5 "crates/fixtures",
6 "crates/hir",
7 "crates/hir-ssa",
8 + "crates/hir-optimization",
9 "crates/swc-demo",
10 "crates/estree",
11 "crates/estree-codegen",
compiler/forget/crates/build-hir/src/builder.rs
+18 -14
@@ -118,14 +118,8 @@ impl<'a> Builder<'a> {
118 blocks: self.completed,
119 instructions: self.instructions,
120 };
121 -
122 - reverse_postorder_blocks(&mut hir);
123 - remove_unreachable_for_updates(&mut hir);
124 - remove_unreachable_fallthroughs(&mut hir);
125 - remove_unreachable_do_while_statements(&mut hir);
126 - mark_instruction_ids(&mut hir)?;
127 - mark_predecessors(&mut hir);
128 -
121 + // Run all the initialization passes
122 + initialize_hir(&mut hir)?;
123 Ok(hir)
124 }
125
@@ -362,9 +356,19 @@ impl<'a> Builder<'a> {
356 }
357 }
358
359 +pub fn initialize_hir<'a>(hir: &mut HIR<'a>) -> Result<(), BuildDiagnostic> {
360 + reverse_postorder_blocks(hir);
361 + remove_unreachable_for_updates(hir);
362 + remove_unreachable_fallthroughs(hir);
363 + remove_unreachable_do_while_statements(hir);
364 + mark_instruction_ids(hir)?;
365 + mark_predecessors(hir);
366 + Ok(())
367 +}
368 +
369 /// Modifies the HIR to put the blocks in reverse postorder, with predecessors before
370 /// successors (except for the case of loops)
367 -fn reverse_postorder_blocks<'a>(hir: &mut HIR<'a>) {
371 +pub fn reverse_postorder_blocks<'a>(hir: &mut HIR<'a>) {
372 let mut visited = HashSet::<BlockId>::with_capacity(hir.blocks.len());
373 let mut postorder = std::vec::Vec::<BlockId>::with_capacity(hir.blocks.len());
374 fn visit<'a>(
@@ -416,7 +420,7 @@ fn reverse_postorder_blocks<'a>(hir: &mut HIR<'a>) {
420 }
421
422 /// Prunes ForTerminal.update values (sets to None) if they are unreachable
419 -fn remove_unreachable_for_updates<'a>(hir: &mut HIR<'a>) {
423 +pub fn remove_unreachable_for_updates<'a>(hir: &mut HIR<'a>) {
424 let block_ids: HashSet<BlockId> = hir.blocks.keys().cloned().collect();
425
426 for block in hir.blocks.values_mut() {
@@ -432,7 +436,7 @@ fn remove_unreachable_for_updates<'a>(hir: &mut HIR<'a>) {
436
437 /// Prunes unreachable fallthrough values, setting them to None if the referenced
438 /// block was not otherwise reachable.
435 -fn remove_unreachable_fallthroughs<'a>(hir: &mut HIR<'a>) {
439 +pub fn remove_unreachable_fallthroughs<'a>(hir: &mut HIR<'a>) {
440 let block_ids: HashSet<BlockId> = hir.blocks.keys().cloned().collect();
441
442 for block in hir.blocks.values_mut() {
@@ -450,7 +454,7 @@ fn remove_unreachable_fallthroughs<'a>(hir: &mut HIR<'a>) {
454 }
455
456 /// Rewrites DoWhile statements into Gotos if the test block is not reachable
453 -fn remove_unreachable_do_while_statements<'a>(hir: &mut HIR<'a>) {
457 +pub fn remove_unreachable_do_while_statements<'a>(hir: &mut HIR<'a>) {
458 let block_ids: HashSet<BlockId> = hir.blocks.keys().cloned().collect();
459
460 for block in hir.blocks.values_mut() {
@@ -467,7 +471,7 @@ fn remove_unreachable_do_while_statements<'a>(hir: &mut HIR<'a>) {
471
472 /// Updates the instruction ids for all instructions and blocks
473 /// Relies on the blocks being in reverse postorder to ensure that id ordering is correct
470 -fn mark_instruction_ids<'a>(hir: &mut HIR<'a>) -> Result<(), BuildDiagnostic> {
474 +pub fn mark_instruction_ids<'a>(hir: &mut HIR<'a>) -> Result<(), BuildDiagnostic> {
475 let mut id_gen = InstructionIdGenerator::new();
476 let mut visited = HashSet::<(usize, usize)>::new();
477 for (ii, block) in hir.blocks.values_mut().enumerate() {
@@ -489,7 +493,7 @@ fn mark_instruction_ids<'a>(hir: &mut HIR<'a>) -> Result<(), BuildDiagnostic> {
493 }
494
495 /// Updates the predecessors of each block
492 -fn mark_predecessors<'a>(hir: &mut HIR<'a>) {
496 +pub fn mark_predecessors<'a>(hir: &mut HIR<'a>) {
497 for block in hir.blocks.values_mut() {
498 block.predecessors.clear();
499 }
compiler/forget/crates/build-hir/src/lib.rs
+5
@@ -3,4 +3,9 @@ mod builder;
3 mod error;
4
5 pub use build::build;
6 +pub use builder::{
7 + initialize_hir, mark_instruction_ids, mark_predecessors,
8 + remove_unreachable_do_while_statements, remove_unreachable_fallthroughs,
9 + remove_unreachable_for_updates, reverse_postorder_blocks,
10 +};
11 pub use error::*;
compiler/forget/crates/estree-swc/src/lib.rs
+1 -1
@@ -457,7 +457,7 @@ fn convert_binary_operator(op: BinaryOp) -> Operator {
457 BinaryOp::Lt => Operator::Binary(estree::BinaryOperator::LessThan),
458 BinaryOp::LtEq => Operator::Binary(estree::BinaryOperator::LessThanOrEqual),
459 BinaryOp::Mod => Operator::Binary(estree::BinaryOperator::Modulo),
460 - // BinaryOp::Mul => Operator::Binary(estree::BinaryOperator::Asterisk),
460 + BinaryOp::Mul => Operator::Binary(estree::BinaryOperator::Multiply),
461 BinaryOp::NotEq => Operator::Binary(estree::BinaryOperator::NotEquals),
462 BinaryOp::NotEqEq => Operator::Binary(estree::BinaryOperator::NotStrictEquals),
463 BinaryOp::RShift => Operator::Binary(estree::BinaryOperator::ShiftRight),
compiler/forget/crates/fixtures/Cargo.toml
+1
@@ -11,6 +11,7 @@ insta = "1.30.0"
11 estree = { path = "../estree" }
12 estree-swc = { path = "../estree-swc" }
13 hir = { path = "../hir" }
14 +hir-optimization = { path = "../hir-optimization" }
15 hir-ssa = { path = "../hir-ssa" }
16 build-hir = { path = "../build-hir" }
17 bumpalo = { version = "3.13.0", features = ["collections"] }
compiler/forget/crates/fixtures/tests/fixtures/constant-propagation-constant-if-condition.js new
+17
@@ -0,0 +1,17 @@
1 +function Component(props) {
2 + let x = true;
3 + let y;
4 + if (x) {
5 + y = 42;
6 + } else {
7 + y = "nope";
8 + }
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;
17 +}
compiler/forget/crates/fixtures/tests/fixtures/constant-propagation.js new
+60
@@ -0,0 +1,60 @@
1 +function Component(props) {
2 + // global propagation
3 + let a;
4 + a = Math;
5 + a; // Math
6 +
7 + // primitive propagation w phi
8 + let b;
9 + if (props) {
10 + b = true;
11 + } else {
12 + b = true;
13 + }
14 + b; // true
15 +
16 + // primitive propagation fails if different values
17 + let c;
18 + if (props) {
19 + c = true;
20 + } else {
21 + c = 42;
22 + }
23 + c; // <no change>
24 +
25 + // constant evaluation
26 + 42 + 1; // 43
27 + 42 - 1; // 41
28 + 42 * 2; // 84
29 + 42 / 2; // 21
30 + 0 == 1; // false
31 + 0 != 1; // true
32 + 0 === 1; // false
33 + 0 !== 1; // true
34 + 0 == 0; // true
35 + // TODO: unary operators
36 + // 0 == -0; // false
37 + // 0 != -0; // true
38 + // 0 === -0; // false
39 + // 0 !== -0; // true
40 + NaN == NaN; // false
41 + NaN != NaN; // true
42 + NaN !== NaN; // true
43 + NaN !== NaN; // true
44 + "hello" == "hello"; // true
45 + "hello" != "hello"; // false
46 + "hello" === "hello"; // true
47 + "hello" !== "hello"; // false
48 + "hello" == "world"; // false
49 + "hello" != "world"; // true
50 + "hello" === "world"; // false
51 + "hello" !== "world"; // true
52 + true == true; // true
53 + true != true; // false
54 + true === true; // true
55 + true !== true; // false
56 +
57 + // constant evaluation through variable
58 + let x = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
59 + x;
60 +}
compiler/forget/crates/fixtures/tests/fixtures_test.rs
+3
@@ -5,6 +5,7 @@ use bumpalo::Bump;
5 use estree::{ModuleItem, Statement};
6 use estree_swc::parse;
7 use hir::{Environment, Print, Registry};
8 +use hir_optimization::constant_propagation;
9 use hir_ssa::{eliminate_redundant_phis, enter_ssa};
10 use insta::{assert_snapshot, glob};
11 use miette::{NamedSource, Report};
@@ -12,6 +13,7 @@ use miette::{NamedSource, Report};
13 #[test]
14 fn fixtures() {
15 glob!("fixtures/**.js", |path| {
16 + println!("fixture {}", path.to_str().unwrap());
17 let input = std::fs::read_to_string(path).unwrap();
18 let ast = parse(&input, path.to_str().unwrap()).unwrap();
19
@@ -35,6 +37,7 @@ fn fixtures() {
37 Ok(mut fun) => {
38 enter_ssa(&environment, &mut fun).unwrap();
39 eliminate_redundant_phis(&environment, &mut fun);
40 + constant_propagation(&environment, &mut fun);
41 fun.print(&fun.body, &mut output).unwrap();
42 }
43 Err(error) => {
compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@constant-propagation-constant-if-condition.js.snap new
+39
@@ -0,0 +1,39 @@
1 +---
2 +source: crates/fixtures/tests/fixtures_test.rs
3 +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;
12 + } else {
13 + y = "nope";
14 + }
15 + return y;
16 +}
17 +
18 +
19 +Output:
20 +function Component(
21 + unknown props$3,
22 +)
23 +entry bb0
24 +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
30 +bb2 (block)
31 + predecessors: bb0
32 + [5] #3 = 42
33 + [6] #4 = StoreLocal Reassign unknown y$6 = unknown #3
34 + [7] Goto bb1
35 +bb1 (block)
36 + predecessors: bb2
37 + [8] #8 = LoadLocal unknown y$6
38 + [9] Return unknown #8
39 +
compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@constant-propagation.js.snap new
+199
@@ -0,0 +1,199 @@
1 +---
2 +source: crates/fixtures/tests/fixtures_test.rs
3 +expression: "format!(\"Input:\\n{input}\\n\\nOutput:\\n{output}\")"
4 +input_file: crates/fixtures/tests/fixtures/constant-propagation.js
5 +---
6 +Input:
7 +function Component(props) {
8 + // global propagation
9 + let a;
10 + a = Math;
11 + a; // Math
12 +
13 + // primitive propagation w phi
14 + let b;
15 + if (props) {
16 + b = true;
17 + } else {
18 + b = true;
19 + }
20 + b; // true
21 +
22 + // primitive propagation fails if different values
23 + let c;
24 + if (props) {
25 + c = true;
26 + } else {
27 + c = 42;
28 + }
29 + c; // <no change>
30 +
31 + // constant evaluation
32 + 42 + 1; // 43
33 + 42 - 1; // 41
34 + 42 * 2; // 84
35 + 42 / 2; // 21
36 + 0 == 1; // false
37 + 0 != 1; // true
38 + 0 === 1; // false
39 + 0 !== 1; // true
40 + 0 == 0; // true
41 + // TODO: unary operators
42 + // 0 == -0; // false
43 + // 0 != -0; // true
44 + // 0 === -0; // false
45 + // 0 !== -0; // true
46 + NaN == NaN; // false
47 + NaN != NaN; // true
48 + NaN !== NaN; // true
49 + NaN !== NaN; // true
50 + "hello" == "hello"; // true
51 + "hello" != "hello"; // false
52 + "hello" === "hello"; // true
53 + "hello" !== "hello"; // false
54 + "hello" == "world"; // false
55 + "hello" != "world"; // true
56 + "hello" === "world"; // false
57 + "hello" !== "world"; // true
58 + true == true; // true
59 + true != true; // false
60 + true === true; // true
61 + true !== true; // false
62 +
63 + // constant evaluation through variable
64 + let x = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
65 + x;
66 +}
67 +
68 +
69 +Output:
70 +function Component(
71 + unknown props$5,
72 +)
73 +entry bb0
74 +bb0 (block)
75 + [0] #0 = DeclareLocal Let unknown a$6
76 + [1] #1 = LoadGlobal Math
77 + [2] #2 = StoreLocal Reassign unknown a$7 = unknown #1
78 + [3] #3 = LoadGlobal Math
79 + [4] #4 = DeclareLocal Let unknown b$8
80 + [5] #9 = LoadLocal unknown props$5
81 + [6] If unknown #9 consequent=bb2 alternate=bb3 fallthrough=bb1
82 +bb2 (block)
83 + predecessors: bb0
84 + [7] #5 = true
85 + [8] #6 = StoreLocal Reassign unknown b$9 = unknown #5
86 + [9] Goto bb1
87 +bb3 (block)
88 + predecessors: bb0
89 + [10] #7 = true
90 + [11] #8 = StoreLocal Reassign unknown b$10 = unknown #7
91 + [12] Goto bb1
92 +bb1 (block)
93 + predecessors: bb2, bb3
94 + b$11: phi(bb2: b$9, bb3: b$10)
95 + [13] #10 = true
96 + [14] #11 = DeclareLocal Let unknown c$12
97 + [15] #16 = LoadLocal unknown props$5
98 + [16] If unknown #16 consequent=bb5 alternate=bb6 fallthrough=bb4
99 +bb5 (block)
100 + predecessors: bb1
101 + [17] #12 = true
102 + [18] #13 = StoreLocal Reassign unknown c$14 = unknown #12
103 + [19] Goto bb4
104 +bb6 (block)
105 + predecessors: bb1
106 + [20] #14 = 42
107 + [21] #15 = StoreLocal Reassign unknown c$15 = unknown #14
108 + [22] Goto bb4
109 +bb4 (block)
110 + predecessors: bb5, bb6
111 + c$16: phi(bb5: c$14, bb6: c$15)
112 + [23] #17 = LoadLocal unknown c$16
113 + [24] #18 = 42
114 + [25] #19 = 1
115 + [26] #20 = 43
116 + [27] #21 = 42
117 + [28] #22 = 1
118 + [29] #23 = 41
119 + [30] #24 = 42
120 + [31] #25 = 2
121 + [32] #26 = 84
122 + [33] #27 = 42
123 + [34] #28 = 2
124 + [35] #29 = 21
125 + [36] #30 = 0
126 + [37] #31 = 1
127 + [38] #32 = false
128 + [39] #33 = 0
129 + [40] #34 = 1
130 + [41] #35 = true
131 + [42] #36 = 0
132 + [43] #37 = 1
133 + [44] #38 = false
134 + [45] #39 = 0
135 + [46] #40 = 1
136 + [47] #41 = true
137 + [48] #42 = 0
138 + [49] #43 = 0
139 + [50] #44 = true
140 + [51] #45 = LoadGlobal NaN
141 + [52] #46 = LoadGlobal NaN
142 + [53] #47 = Binary unknown #45 == unknown #46
143 + [54] #48 = LoadGlobal NaN
144 + [55] #49 = LoadGlobal NaN
145 + [56] #50 = Binary unknown #48 != unknown #49
146 + [57] #51 = LoadGlobal NaN
147 + [58] #52 = LoadGlobal NaN
148 + [59] #53 = Binary unknown #51 !== unknown #52
149 + [60] #54 = LoadGlobal NaN
150 + [61] #55 = LoadGlobal NaN
151 + [62] #56 = Binary unknown #54 !== unknown #55
152 + [63] #57 = "hello"
153 + [64] #58 = "hello"
154 + [65] #59 = true
155 + [66] #60 = "hello"
156 + [67] #61 = "hello"
157 + [68] #62 = false
158 + [69] #63 = "hello"
159 + [70] #64 = "hello"
160 + [71] #65 = true
161 + [72] #66 = "hello"
162 + [73] #67 = "hello"
163 + [74] #68 = false
164 + [75] #69 = "hello"
165 + [76] #70 = "world"
166 + [77] #71 = false
167 + [78] #72 = "hello"
168 + [79] #73 = "world"
169 + [80] #74 = true
170 + [81] #75 = "hello"
171 + [82] #76 = "world"
172 + [83] #77 = false
173 + [84] #78 = "hello"
174 + [85] #79 = "world"
175 + [86] #80 = true
176 + [87] #81 = true
177 + [88] #82 = true
178 + [89] #83 = true
179 + [90] #84 = true
180 + [91] #85 = true
181 + [92] #86 = false
182 + [93] #87 = true
183 + [94] #88 = true
184 + [95] #89 = true
185 + [96] #90 = true
186 + [97] #91 = true
187 + [98] #92 = false
188 + [99] #93 = 5
189 + [100] #94 = 60
190 + [101] #95 = 300
191 + [102] #96 = 60
192 + [103] #97 = 18000
193 + [104] #98 = 1000
194 + [105] #99 = 18000000
195 + [106] #100 = StoreLocal Let unknown x$17 = unknown #99
196 + [107] #101 = 18000000
197 + [108] #102 = <undefined>
198 + [109] Return unknown #102
199 +
compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@identifiers.js.snap
+1 -1
@@ -34,7 +34,7 @@ bb0 (block)
34 [3] #3 = StoreLocal Let unknown y$5 = unknown #2
35 [4] #4 = false
36 [5] #5 = StoreLocal Reassign unknown y$6 = unknown #4
37 - [6] #6 = LoadLocal unknown y$6
37 + [6] #6 = false
38 [7] #7 = DeclareLocal Let unknown z$7
39 [8] #8 = LoadLocal unknown z$7
40 [9] #9 = LoadLocal unknown x$4
compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@ssa-reassign-if.js.snap
+1 -1
@@ -66,7 +66,7 @@ bb1 (block)
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
69 - [22] #16 = LoadLocal unknown y$8
69 + [22] #16 = 0
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-optimization/Cargo.toml new
+17
@@ -0,0 +1,17 @@
1 +[package]
2 +name = "hir-optimization"
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 +estree = { path = "../estree" }
10 +hir = { path = "../hir" }
11 +hir-ssa = { path = "../hir-ssa" }
12 +build-hir = { path = "../build-hir" }
13 +utils = { path = "../utils" }
14 +bumpalo = "3.13.0"
15 +indexmap = "2.0.0"
16 +miette = { version = "5.9.0" }
17 +thiserror = "1.0.41"
\ No newline at end of file
compiler/forget/crates/hir-optimization/src/constant_propagation.rs new
+258
@@ -0,0 +1,258 @@
1 +use std::collections::HashMap;
2 +
3 +use build_hir::initialize_hir;
4 +use estree::BinaryOperator;
5 +use hir::{
6 + BlockKind, Environment, Function, GotoKind, IdentifierId, Instruction, InstructionValue,
7 + LoadGlobal, Operand, Primitive, PrimitiveValue, TerminalValue,
8 +};
9 +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 {
15 + // If terminals have changed then blocks may have become newly unreachable,
16 + // so reinitialize the HIR
17 + // TODO handle errors
18 + initialize_hir(&mut fun.body).unwrap();
19 +
20 + // Now that predecessors have changed, prune phi operands for unreachable blocks
21 + // for example, a phi node whose operand was eliminated because it was set in a
22 + // block that is no longer reached
23 + for (_, block) in fun.body.blocks.iter_mut() {
24 + // TODO: avoid the clone here
25 + let predecessors = block.predecessors.clone();
26 + for phi in block.phis.iter_mut() {
27 + phi.operands
28 + .retain(|predecessor, _| predecessors.contains(predecessor))
29 + }
30 + }
31 +
32 + // By removing some phi operands, there may be phis that were not previously
33 + // redundant but now are
34 + eliminate_redundant_phis(env, fun);
35 + }
36 +}
37 +
38 +fn apply_constant_propagation<'a>(
39 + env: &Environment<'a>,
40 + fun: &mut Function<'a>,
41 + constants: &mut Constants<'a>,
42 +) -> bool {
43 + let mut has_changes = false;
44 +
45 + for (_, block) in fun.body.blocks.iter_mut() {
46 + for phi in block.phis.iter() {
47 + let mut value: Option<Constant<'a>> = None;
48 + for (_, operand) in &phi.operands {
49 + if let Some(operand_value) = constants.get(&operand.id) {
50 + match &mut value {
51 + Some(value) if value == operand_value => {
52 + // no-op
53 + }
54 + Some(_) => {
55 + value = None;
56 + break;
57 + }
58 + None => {
59 + value = Some(operand_value.clone());
60 + }
61 + }
62 + } else {
63 + // This phi operand's value is unknown, bail out of replacing it
64 + value = None;
65 + break;
66 + }
67 + }
68 + if let Some(value) = value {
69 + constants.insert(phi.identifier.id, value);
70 + }
71 + }
72 + for (ix, instr_ix) in block.instructions.iter().enumerate() {
73 + if block.kind == BlockKind::Sequence && ix == block.instructions.len() - 1 {
74 + // Evaluating the last value of a sequence can break order of evaluation
75 + // so skip these instructions
76 + continue;
77 + }
78 + let instr_ix = usize::from(*instr_ix);
79 + let mut instr = std::mem::replace(
80 + &mut fun.body.instructions[instr_ix].value,
81 + InstructionValue::Tombstone,
82 + );
83 + evaluate_instruction(env, &fun.body.instructions, &mut instr, constants);
84 + fun.body.instructions[instr_ix].value = instr;
85 + }
86 +
87 + if block.kind != BlockKind::Block {
88 + // can't rewrite terminals in value blocks yet
89 + continue;
90 + }
91 +
92 + if let TerminalValue::If(terminal) = &mut block.terminal.value {
93 + if let Some(primitive) =
94 + read_primitive_instruction(&fun.body.instructions, &terminal.test)
95 + {
96 + let target_block_id = if primitive.value.is_truthy() {
97 + terminal.consequent
98 + } else {
99 + terminal.alternate
100 + };
101 + block.terminal.value = TerminalValue::Goto(hir::GotoTerminal {
102 + block: target_block_id,
103 + kind: GotoKind::Break,
104 + });
105 + has_changes = true;
106 + }
107 + }
108 + }
109 +
110 + has_changes
111 +}
112 +
113 +fn read_primitive_instruction<'a>(
114 + instrs: &[Instruction<'a>],
115 + operand: &Operand,
116 +) -> Option<Primitive<'a>> {
117 + let instr = &instrs[usize::from(operand.ix)].value;
118 + if let InstructionValue::Primitive(primitive) = instr {
119 + Some(primitive.clone())
120 + } else {
121 + None
122 + }
123 +}
124 +
125 +fn evaluate_instruction<'a>(
126 + env: &Environment<'a>,
127 + instrs: &[Instruction<'a>],
128 + mut instr: &mut InstructionValue<'a>,
129 + constants: &mut Constants<'a>,
130 +) {
131 + let read_constant = |operand: &Operand| {
132 + let instr = &instrs[usize::from(operand.ix)].value;
133 + match instr {
134 + InstructionValue::Primitive(value) => Some(Constant::Primitive(value.clone())),
135 + InstructionValue::LoadGlobal(value) => Some(Constant::Global(value.clone())),
136 + _ => None,
137 + }
138 + };
139 + match &mut instr {
140 + InstructionValue::Binary(value) => {
141 + let left = read_primitive_instruction(instrs, &value.left);
142 + let right = read_primitive_instruction(instrs, &value.right);
143 + match (left, right) {
144 + (Some(left), Some(right)) => {
145 + if let Some(result) = apply_binary_operator(env, left, value.operator, right) {
146 + *instr = InstructionValue::Primitive(result);
147 + }
148 + }
149 + _ => {
150 + // no-op, not all operands are known
151 + }
152 + }
153 + }
154 + InstructionValue::LoadLocal(value) => {
155 + if let Some(const_value) = constants.get(&value.place.identifier.id) {
156 + *instr = const_value.into();
157 + }
158 + }
159 + InstructionValue::StoreLocal(value) => {
160 + if let Some(const_value) = read_constant(&value.value) {
161 + constants.insert(value.lvalue.identifier.identifier.id, const_value);
162 + }
163 + }
164 + _ => {
165 + // no-op, not all instructions can be processed
166 + }
167 + }
168 +}
169 +
170 +fn apply_binary_operator<'a>(
171 + env: &Environment<'a>,
172 + left: Primitive<'a>,
173 + operator: BinaryOperator,
174 + right: Primitive<'a>,
175 +) -> Option<Primitive<'a>> {
176 + match (left.value, right.value) {
177 + (PrimitiveValue::Number(left), PrimitiveValue::Number(right)) => match operator {
178 + BinaryOperator::Add => Some(Primitive {
179 + value: PrimitiveValue::Number(left + right),
180 + }),
181 + BinaryOperator::Subtract => Some(Primitive {
182 + value: PrimitiveValue::Number(left - right),
183 + }),
184 + BinaryOperator::Multiply => Some(Primitive {
185 + value: PrimitiveValue::Number(left * right),
186 + }),
187 + BinaryOperator::Divide => Some(Primitive {
188 + value: PrimitiveValue::Number(left / right),
189 + }),
190 + BinaryOperator::LessThan => Some(Primitive {
191 + value: PrimitiveValue::Boolean(left < right),
192 + }),
193 + BinaryOperator::LessThanOrEqual => Some(Primitive {
194 + value: PrimitiveValue::Boolean(left <= right),
195 + }),
196 + BinaryOperator::GreaterThan => Some(Primitive {
197 + value: PrimitiveValue::Boolean(left > right),
198 + }),
199 + BinaryOperator::GreaterThanOrEqual => Some(Primitive {
200 + value: PrimitiveValue::Boolean(left >= right),
201 + }),
202 + BinaryOperator::Equals => Some(Primitive {
203 + value: PrimitiveValue::Boolean(left.equals(right)),
204 + }),
205 + BinaryOperator::NotEquals => Some(Primitive {
206 + value: PrimitiveValue::Boolean(left.not_equals(right)),
207 + }),
208 + BinaryOperator::StrictEquals => Some(Primitive {
209 + value: PrimitiveValue::Boolean(left.equals(right)),
210 + }),
211 + BinaryOperator::NotStrictEquals => Some(Primitive {
212 + value: PrimitiveValue::Boolean(left.not_equals(right)),
213 + }),
214 + _ => None,
215 + },
216 + (left, right) => match operator {
217 + BinaryOperator::Equals => left.loosely_equals(&right).map(|value| Primitive {
218 + value: PrimitiveValue::Boolean(value),
219 + }),
220 + BinaryOperator::NotEquals => left.not_loosely_equals(&right).map(|value| Primitive {
221 + value: PrimitiveValue::Boolean(value),
222 + }),
223 + BinaryOperator::StrictEquals => Some(Primitive {
224 + value: PrimitiveValue::Boolean(left.strictly_equals(&right)),
225 + }),
226 + BinaryOperator::NotStrictEquals => Some(Primitive {
227 + value: PrimitiveValue::Boolean(left.not_strictly_equals(&right)),
228 + }),
229 + _ => None,
230 + },
231 + }
232 +}
233 +
234 +type Constants<'a> = HashMap<IdentifierId, Constant<'a>>;
235 +
236 +#[derive(Debug, Clone, PartialEq, Eq)]
237 +enum Constant<'a> {
238 + Global(LoadGlobal<'a>),
239 + Primitive(Primitive<'a>),
240 +}
241 +
242 +impl<'a> From<&Constant<'a>> for InstructionValue<'a> {
243 + fn from(value: &Constant<'a>) -> Self {
244 + match value {
245 + Constant::Global(value) => InstructionValue::LoadGlobal(value.clone()),
246 + Constant::Primitive(value) => InstructionValue::Primitive(value.clone()),
247 + }
248 + }
249 +}
250 +
251 +impl<'a> From<Constant<'a>> for InstructionValue<'a> {
252 + fn from(value: Constant<'a>) -> Self {
253 + match value {
254 + Constant::Global(value) => InstructionValue::LoadGlobal(value),
255 + Constant::Primitive(value) => InstructionValue::Primitive(value),
256 + }
257 + }
258 +}
compiler/forget/crates/hir-optimization/src/lib.rs new
+3
@@ -0,0 +1,3 @@
1 +mod constant_propagation;
2 +
3 +pub use constant_propagation::constant_propagation;
compiler/forget/crates/hir-ssa/src/eliminate_redundant_phis.rs
+1 -1
@@ -16,7 +16,7 @@ use utils::RetainMut;
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>) {
19 +pub fn eliminate_redundant_phis<'a>(_env: &Environment, fun: &mut Function<'a>) {
20 let hir = &mut fun.body;
21 let mut rewrites = Rewrites::new();
22
compiler/forget/crates/hir/src/instruction.rs
+130 -8
@@ -32,6 +32,7 @@ impl<'a> Instruction<'a> {
32 InstructionValue::StoreLocal(instr) => {
33 f(&mut instr.lvalue);
34 }
35 + InstructionValue::Tombstone => {}
36 }
37 }
38
@@ -49,6 +50,7 @@ impl<'a> Instruction<'a> {
50 InstructionValue::LoadLocal(instr) => f(&mut instr.place),
51 InstructionValue::Primitive(_) => {}
52 InstructionValue::StoreLocal(_) => {}
53 + InstructionValue::Tombstone => {}
54 }
55 }
56 }
@@ -87,7 +89,7 @@ pub enum InstructionValue<'a> {
89 // Template(Template<'a>),
90 // TypeCast(TypeCast<'a>),
91 // Unary(Unary<'a>),
90 - // Unsupported(Unsupported<'a>),
92 + Tombstone,
93 }
94
95 #[derive(Debug)]
@@ -108,12 +110,12 @@ pub struct Binary {
110 pub right: Operand,
111 }
112
111 -#[derive(Debug)]
113 +#[derive(Debug, Clone, PartialEq, Eq)]
114 pub struct Primitive<'a> {
115 pub value: PrimitiveValue<'a>,
116 }
117
116 -#[derive(Debug)]
118 +#[derive(Debug, Clone, PartialEq, Eq)]
119 pub enum PrimitiveValue<'a> {
120 Boolean(bool),
121 Null,
@@ -122,20 +124,140 @@ pub enum PrimitiveValue<'a> {
124 Undefined,
125 }
126
127 +impl<'a> PrimitiveValue<'a> {
128 + pub fn is_truthy(&self) -> bool {
129 + match &self {
130 + PrimitiveValue::Boolean(value) => *value,
131 + PrimitiveValue::Number(value) => value.is_truthy(),
132 + PrimitiveValue::String(value) => value.len() != 0,
133 + PrimitiveValue::Null => false,
134 + PrimitiveValue::Undefined => false,
135 + }
136 + }
137 +
138 + // Partial implementation of loose equality for javascript, returns Some for supported
139 + // cases w the equality result, and None for unsupported cases
140 + pub fn loosely_equals(&self, other: &Self) -> Option<bool> {
141 + // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal
142 + match (&self, &other) {
143 + // 1. If Type(x) is Type(y), then
144 + // a. Return IsStrictlyEqual(x, y).
145 + (PrimitiveValue::Number(left), PrimitiveValue::Number(right)) => {
146 + Some(left.equals(*right))
147 + }
148 + (PrimitiveValue::Null, PrimitiveValue::Null) => Some(true),
149 + (PrimitiveValue::Undefined, PrimitiveValue::Undefined) => Some(true),
150 + (PrimitiveValue::Boolean(left), PrimitiveValue::Boolean(right)) => Some(left == right),
151 + (PrimitiveValue::String(left), PrimitiveValue::String(right)) => Some(left == right),
152 +
153 + // 2. If x is null and y is undefined, return true.
154 + (PrimitiveValue::Null, PrimitiveValue::Undefined) => Some(true),
155 +
156 + // 3. If x is undefined and y is null, return true.
157 + (PrimitiveValue::Undefined, PrimitiveValue::Null) => Some(true),
158 + _ => None,
159 + }
160 + }
161 +
162 + pub fn not_loosely_equals(&self, other: &Self) -> Option<bool> {
163 + self.loosely_equals(other).map(|value| !value)
164 + }
165 +
166 + // Complete implementation of strict equality for javascript
167 + pub fn strictly_equals(&self, other: &Self) -> bool {
168 + // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-isstrictlyequal
169 + match (&self, &other) {
170 + (PrimitiveValue::Number(left), PrimitiveValue::Number(right)) => left.equals(*right),
171 + (PrimitiveValue::Null, PrimitiveValue::Null) => true,
172 + (PrimitiveValue::Undefined, PrimitiveValue::Undefined) => true,
173 + (PrimitiveValue::Boolean(left), PrimitiveValue::Boolean(right)) => left == right,
174 + (PrimitiveValue::String(left), PrimitiveValue::String(right)) => left == right,
175 + _ => false,
176 + }
177 + }
178 +
179 + pub fn not_strictly_equals(&self, other: &Self) -> bool {
180 + !self.strictly_equals(other)
181 + }
182 +}
183 +
184 /// Represents a JavaScript Number as its binary representation so that
185 /// -1 == -1, NaN == Nan etc.
127 -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
186 +/// Note: NaN is *always* represented as the f64::NAN constant to allow
187 +/// comparison of NaNs.
188 +#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Debug, Hash)]
189 pub struct Number(u64);
190
191 impl From<f64> for Number {
192 fn from(value: f64) -> Self {
132 - Self(value.to_bits())
193 + if value.is_nan() {
194 + Self(f64::NAN.to_bits())
195 + } else {
196 + Self(value.to_bits())
197 + }
198 }
199 }
200
201 impl From<Number> for f64 {
137 - fn from(value: Number) -> Self {
138 - f64::from_bits(value.0)
202 + fn from(number: Number) -> Self {
203 + let value = f64::from_bits(number.0);
204 + assert!(!f64::is_nan(value) || number.0 == f64::NAN.to_bits());
205 + value
206 + }
207 +}
208 +
209 +impl Number {
210 + pub fn equals(self, other: Self) -> bool {
211 + f64::from(self) == f64::from(other)
212 + }
213 +
214 + pub fn not_equals(self, other: Self) -> bool {
215 + !self.equals(other)
216 + }
217 +
218 + pub fn is_truthy(self) -> bool {
219 + let value = f64::from(self);
220 + if self.0 == f64::NAN.to_bits() || value == 0.0 || value == -0.0 {
221 + false
222 + } else {
223 + true
224 + }
225 + }
226 +}
227 +
228 +impl std::ops::Add for Number {
229 + type Output = Number;
230 +
231 + fn add(self, rhs: Self) -> Self::Output {
232 + let result = f64::from(self) + f64::from(rhs);
233 + Self::from(result)
234 + }
235 +}
236 +
237 +impl std::ops::Sub for Number {
238 + type Output = Number;
239 +
240 + fn sub(self, rhs: Self) -> Self::Output {
241 + let result = f64::from(self) - f64::from(rhs);
242 + Self::from(result)
243 + }
244 +}
245 +
246 +impl std::ops::Mul for Number {
247 + type Output = Number;
248 +
249 + fn mul(self, rhs: Self) -> Self::Output {
250 + let result = f64::from(self) * f64::from(rhs);
251 + Self::from(result)
252 + }
253 +}
254 +
255 +impl std::ops::Div for Number {
256 + type Output = Number;
257 +
258 + fn div(self, rhs: Self) -> Self::Output {
259 + let result = f64::from(self) / f64::from(rhs);
260 + Self::from(result)
261 }
262 }
263
@@ -149,7 +271,7 @@ pub struct LoadContext {
271 pub place: Operand,
272 }
273
152 -#[derive(Debug)]
274 +#[derive(Debug, PartialEq, Eq, Clone)]
275 pub struct LoadGlobal<'a> {
276 pub name: String<'a>,
277 }
compiler/forget/crates/hir/src/print.rs
+3
@@ -146,6 +146,9 @@ impl<'a> Print<'a> for InstructionValue<'a> {
146 write!(out, " {} ", value.operator)?;
147 value.right.print(hir, out)?;
148 }
149 + InstructionValue::Tombstone => {
150 + write!(out, "Tombstone!")?;
151 + }
152 _ => write!(out, "{:?}", self)?,
153 }
154 Ok(())