mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[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.
This commit is contained in:
Generated
+16
@@ -504,6 +504,7 @@ dependencies = [
|
||||
"estree",
|
||||
"estree-swc",
|
||||
"hir",
|
||||
"hir-optimization",
|
||||
"hir-ssa",
|
||||
"insta",
|
||||
"miette 5.9.0",
|
||||
@@ -639,6 +640,21 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hir-optimization"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"build-hir",
|
||||
"bumpalo",
|
||||
"estree",
|
||||
"hir",
|
||||
"hir-ssa",
|
||||
"indexmap 2.0.0",
|
||||
"miette 5.9.0",
|
||||
"thiserror",
|
||||
"utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hir-ssa"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -5,6 +5,7 @@ members = [
|
||||
"crates/fixtures",
|
||||
"crates/hir",
|
||||
"crates/hir-ssa",
|
||||
"crates/hir-optimization",
|
||||
"crates/swc-demo",
|
||||
"crates/estree",
|
||||
"crates/estree-codegen",
|
||||
|
||||
@@ -118,14 +118,8 @@ impl<'a> Builder<'a> {
|
||||
blocks: self.completed,
|
||||
instructions: self.instructions,
|
||||
};
|
||||
|
||||
reverse_postorder_blocks(&mut hir);
|
||||
remove_unreachable_for_updates(&mut hir);
|
||||
remove_unreachable_fallthroughs(&mut hir);
|
||||
remove_unreachable_do_while_statements(&mut hir);
|
||||
mark_instruction_ids(&mut hir)?;
|
||||
mark_predecessors(&mut hir);
|
||||
|
||||
// Run all the initialization passes
|
||||
initialize_hir(&mut hir)?;
|
||||
Ok(hir)
|
||||
}
|
||||
|
||||
@@ -362,9 +356,19 @@ impl<'a> Builder<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialize_hir<'a>(hir: &mut HIR<'a>) -> Result<(), BuildDiagnostic> {
|
||||
reverse_postorder_blocks(hir);
|
||||
remove_unreachable_for_updates(hir);
|
||||
remove_unreachable_fallthroughs(hir);
|
||||
remove_unreachable_do_while_statements(hir);
|
||||
mark_instruction_ids(hir)?;
|
||||
mark_predecessors(hir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Modifies the HIR to put the blocks in reverse postorder, with predecessors before
|
||||
/// successors (except for the case of loops)
|
||||
fn reverse_postorder_blocks<'a>(hir: &mut HIR<'a>) {
|
||||
pub fn reverse_postorder_blocks<'a>(hir: &mut HIR<'a>) {
|
||||
let mut visited = HashSet::<BlockId>::with_capacity(hir.blocks.len());
|
||||
let mut postorder = std::vec::Vec::<BlockId>::with_capacity(hir.blocks.len());
|
||||
fn visit<'a>(
|
||||
@@ -416,7 +420,7 @@ fn reverse_postorder_blocks<'a>(hir: &mut HIR<'a>) {
|
||||
}
|
||||
|
||||
/// Prunes ForTerminal.update values (sets to None) if they are unreachable
|
||||
fn remove_unreachable_for_updates<'a>(hir: &mut HIR<'a>) {
|
||||
pub fn remove_unreachable_for_updates<'a>(hir: &mut HIR<'a>) {
|
||||
let block_ids: HashSet<BlockId> = hir.blocks.keys().cloned().collect();
|
||||
|
||||
for block in hir.blocks.values_mut() {
|
||||
@@ -432,7 +436,7 @@ fn remove_unreachable_for_updates<'a>(hir: &mut HIR<'a>) {
|
||||
|
||||
/// Prunes unreachable fallthrough values, setting them to None if the referenced
|
||||
/// block was not otherwise reachable.
|
||||
fn remove_unreachable_fallthroughs<'a>(hir: &mut HIR<'a>) {
|
||||
pub fn remove_unreachable_fallthroughs<'a>(hir: &mut HIR<'a>) {
|
||||
let block_ids: HashSet<BlockId> = hir.blocks.keys().cloned().collect();
|
||||
|
||||
for block in hir.blocks.values_mut() {
|
||||
@@ -450,7 +454,7 @@ fn remove_unreachable_fallthroughs<'a>(hir: &mut HIR<'a>) {
|
||||
}
|
||||
|
||||
/// Rewrites DoWhile statements into Gotos if the test block is not reachable
|
||||
fn remove_unreachable_do_while_statements<'a>(hir: &mut HIR<'a>) {
|
||||
pub fn remove_unreachable_do_while_statements<'a>(hir: &mut HIR<'a>) {
|
||||
let block_ids: HashSet<BlockId> = hir.blocks.keys().cloned().collect();
|
||||
|
||||
for block in hir.blocks.values_mut() {
|
||||
@@ -467,7 +471,7 @@ fn remove_unreachable_do_while_statements<'a>(hir: &mut HIR<'a>) {
|
||||
|
||||
/// Updates the instruction ids for all instructions and blocks
|
||||
/// Relies on the blocks being in reverse postorder to ensure that id ordering is correct
|
||||
fn mark_instruction_ids<'a>(hir: &mut HIR<'a>) -> Result<(), BuildDiagnostic> {
|
||||
pub fn mark_instruction_ids<'a>(hir: &mut HIR<'a>) -> Result<(), BuildDiagnostic> {
|
||||
let mut id_gen = InstructionIdGenerator::new();
|
||||
let mut visited = HashSet::<(usize, usize)>::new();
|
||||
for (ii, block) in hir.blocks.values_mut().enumerate() {
|
||||
@@ -489,7 +493,7 @@ fn mark_instruction_ids<'a>(hir: &mut HIR<'a>) -> Result<(), BuildDiagnostic> {
|
||||
}
|
||||
|
||||
/// Updates the predecessors of each block
|
||||
fn mark_predecessors<'a>(hir: &mut HIR<'a>) {
|
||||
pub fn mark_predecessors<'a>(hir: &mut HIR<'a>) {
|
||||
for block in hir.blocks.values_mut() {
|
||||
block.predecessors.clear();
|
||||
}
|
||||
|
||||
@@ -3,4 +3,9 @@ mod builder;
|
||||
mod error;
|
||||
|
||||
pub use build::build;
|
||||
pub use builder::{
|
||||
initialize_hir, mark_instruction_ids, mark_predecessors,
|
||||
remove_unreachable_do_while_statements, remove_unreachable_fallthroughs,
|
||||
remove_unreachable_for_updates, reverse_postorder_blocks,
|
||||
};
|
||||
pub use error::*;
|
||||
|
||||
@@ -457,7 +457,7 @@ fn convert_binary_operator(op: BinaryOp) -> Operator {
|
||||
BinaryOp::Lt => Operator::Binary(estree::BinaryOperator::LessThan),
|
||||
BinaryOp::LtEq => Operator::Binary(estree::BinaryOperator::LessThanOrEqual),
|
||||
BinaryOp::Mod => Operator::Binary(estree::BinaryOperator::Modulo),
|
||||
// BinaryOp::Mul => Operator::Binary(estree::BinaryOperator::Asterisk),
|
||||
BinaryOp::Mul => Operator::Binary(estree::BinaryOperator::Multiply),
|
||||
BinaryOp::NotEq => Operator::Binary(estree::BinaryOperator::NotEquals),
|
||||
BinaryOp::NotEqEq => Operator::Binary(estree::BinaryOperator::NotStrictEquals),
|
||||
BinaryOp::RShift => Operator::Binary(estree::BinaryOperator::ShiftRight),
|
||||
|
||||
@@ -11,6 +11,7 @@ insta = "1.30.0"
|
||||
estree = { path = "../estree" }
|
||||
estree-swc = { path = "../estree-swc" }
|
||||
hir = { path = "../hir" }
|
||||
hir-optimization = { path = "../hir-optimization" }
|
||||
hir-ssa = { path = "../hir-ssa" }
|
||||
build-hir = { path = "../build-hir" }
|
||||
bumpalo = { version = "3.13.0", features = ["collections"] }
|
||||
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
function Component(props) {
|
||||
let x = true;
|
||||
let y;
|
||||
if (x) {
|
||||
y = 42;
|
||||
} else {
|
||||
y = "nope";
|
||||
}
|
||||
// TODO: constant propagate the value of `y` here. we can track which
|
||||
// blocks are reachable as we proceed through, and account for phi
|
||||
// operands for blocks that weren't reached.
|
||||
// something like: track a set of reachable blocks, which populate from
|
||||
// successors of previous block's terminals. but when we see an if w a
|
||||
// constant test value, we only populate as reachable the corresponding
|
||||
// branch's block.
|
||||
return y;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
function Component(props) {
|
||||
// global propagation
|
||||
let a;
|
||||
a = Math;
|
||||
a; // Math
|
||||
|
||||
// primitive propagation w phi
|
||||
let b;
|
||||
if (props) {
|
||||
b = true;
|
||||
} else {
|
||||
b = true;
|
||||
}
|
||||
b; // true
|
||||
|
||||
// primitive propagation fails if different values
|
||||
let c;
|
||||
if (props) {
|
||||
c = true;
|
||||
} else {
|
||||
c = 42;
|
||||
}
|
||||
c; // <no change>
|
||||
|
||||
// constant evaluation
|
||||
42 + 1; // 43
|
||||
42 - 1; // 41
|
||||
42 * 2; // 84
|
||||
42 / 2; // 21
|
||||
0 == 1; // false
|
||||
0 != 1; // true
|
||||
0 === 1; // false
|
||||
0 !== 1; // true
|
||||
0 == 0; // true
|
||||
// TODO: unary operators
|
||||
// 0 == -0; // false
|
||||
// 0 != -0; // true
|
||||
// 0 === -0; // false
|
||||
// 0 !== -0; // true
|
||||
NaN == NaN; // false
|
||||
NaN != NaN; // true
|
||||
NaN !== NaN; // true
|
||||
NaN !== NaN; // true
|
||||
"hello" == "hello"; // true
|
||||
"hello" != "hello"; // false
|
||||
"hello" === "hello"; // true
|
||||
"hello" !== "hello"; // false
|
||||
"hello" == "world"; // false
|
||||
"hello" != "world"; // true
|
||||
"hello" === "world"; // false
|
||||
"hello" !== "world"; // true
|
||||
true == true; // true
|
||||
true != true; // false
|
||||
true === true; // true
|
||||
true !== true; // false
|
||||
|
||||
// constant evaluation through variable
|
||||
let x = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
|
||||
x;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use bumpalo::Bump;
|
||||
use estree::{ModuleItem, Statement};
|
||||
use estree_swc::parse;
|
||||
use hir::{Environment, Print, Registry};
|
||||
use hir_optimization::constant_propagation;
|
||||
use hir_ssa::{eliminate_redundant_phis, enter_ssa};
|
||||
use insta::{assert_snapshot, glob};
|
||||
use miette::{NamedSource, Report};
|
||||
@@ -12,6 +13,7 @@ use miette::{NamedSource, Report};
|
||||
#[test]
|
||||
fn fixtures() {
|
||||
glob!("fixtures/**.js", |path| {
|
||||
println!("fixture {}", path.to_str().unwrap());
|
||||
let input = std::fs::read_to_string(path).unwrap();
|
||||
let ast = parse(&input, path.to_str().unwrap()).unwrap();
|
||||
|
||||
@@ -35,6 +37,7 @@ fn fixtures() {
|
||||
Ok(mut fun) => {
|
||||
enter_ssa(&environment, &mut fun).unwrap();
|
||||
eliminate_redundant_phis(&environment, &mut fun);
|
||||
constant_propagation(&environment, &mut fun);
|
||||
fun.print(&fun.body, &mut output).unwrap();
|
||||
}
|
||||
Err(error) => {
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
---
|
||||
source: crates/fixtures/tests/fixtures_test.rs
|
||||
expression: "format!(\"Input:\\n{input}\\n\\nOutput:\\n{output}\")"
|
||||
input_file: crates/fixtures/tests/fixtures/constant-propagation-constant-if-condition.js
|
||||
---
|
||||
Input:
|
||||
function Component(props) {
|
||||
let x = true;
|
||||
let y;
|
||||
if (x) {
|
||||
y = 42;
|
||||
} else {
|
||||
y = "nope";
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
|
||||
Output:
|
||||
function Component(
|
||||
unknown props$3,
|
||||
)
|
||||
entry bb0
|
||||
bb0 (block)
|
||||
[0] #0 = true
|
||||
[1] #1 = StoreLocal Let unknown x$4 = unknown #0
|
||||
[2] #2 = DeclareLocal Let unknown y$5
|
||||
[3] #7 = true
|
||||
[4] Goto bb2
|
||||
bb2 (block)
|
||||
predecessors: bb0
|
||||
[5] #3 = 42
|
||||
[6] #4 = StoreLocal Reassign unknown y$6 = unknown #3
|
||||
[7] Goto bb1
|
||||
bb1 (block)
|
||||
predecessors: bb2
|
||||
[8] #8 = LoadLocal unknown y$6
|
||||
[9] Return unknown #8
|
||||
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
---
|
||||
source: crates/fixtures/tests/fixtures_test.rs
|
||||
expression: "format!(\"Input:\\n{input}\\n\\nOutput:\\n{output}\")"
|
||||
input_file: crates/fixtures/tests/fixtures/constant-propagation.js
|
||||
---
|
||||
Input:
|
||||
function Component(props) {
|
||||
// global propagation
|
||||
let a;
|
||||
a = Math;
|
||||
a; // Math
|
||||
|
||||
// primitive propagation w phi
|
||||
let b;
|
||||
if (props) {
|
||||
b = true;
|
||||
} else {
|
||||
b = true;
|
||||
}
|
||||
b; // true
|
||||
|
||||
// primitive propagation fails if different values
|
||||
let c;
|
||||
if (props) {
|
||||
c = true;
|
||||
} else {
|
||||
c = 42;
|
||||
}
|
||||
c; // <no change>
|
||||
|
||||
// constant evaluation
|
||||
42 + 1; // 43
|
||||
42 - 1; // 41
|
||||
42 * 2; // 84
|
||||
42 / 2; // 21
|
||||
0 == 1; // false
|
||||
0 != 1; // true
|
||||
0 === 1; // false
|
||||
0 !== 1; // true
|
||||
0 == 0; // true
|
||||
// TODO: unary operators
|
||||
// 0 == -0; // false
|
||||
// 0 != -0; // true
|
||||
// 0 === -0; // false
|
||||
// 0 !== -0; // true
|
||||
NaN == NaN; // false
|
||||
NaN != NaN; // true
|
||||
NaN !== NaN; // true
|
||||
NaN !== NaN; // true
|
||||
"hello" == "hello"; // true
|
||||
"hello" != "hello"; // false
|
||||
"hello" === "hello"; // true
|
||||
"hello" !== "hello"; // false
|
||||
"hello" == "world"; // false
|
||||
"hello" != "world"; // true
|
||||
"hello" === "world"; // false
|
||||
"hello" !== "world"; // true
|
||||
true == true; // true
|
||||
true != true; // false
|
||||
true === true; // true
|
||||
true !== true; // false
|
||||
|
||||
// constant evaluation through variable
|
||||
let x = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
|
||||
x;
|
||||
}
|
||||
|
||||
|
||||
Output:
|
||||
function Component(
|
||||
unknown props$5,
|
||||
)
|
||||
entry bb0
|
||||
bb0 (block)
|
||||
[0] #0 = DeclareLocal Let unknown a$6
|
||||
[1] #1 = LoadGlobal Math
|
||||
[2] #2 = StoreLocal Reassign unknown a$7 = unknown #1
|
||||
[3] #3 = LoadGlobal Math
|
||||
[4] #4 = DeclareLocal Let unknown b$8
|
||||
[5] #9 = LoadLocal unknown props$5
|
||||
[6] If unknown #9 consequent=bb2 alternate=bb3 fallthrough=bb1
|
||||
bb2 (block)
|
||||
predecessors: bb0
|
||||
[7] #5 = true
|
||||
[8] #6 = StoreLocal Reassign unknown b$9 = unknown #5
|
||||
[9] Goto bb1
|
||||
bb3 (block)
|
||||
predecessors: bb0
|
||||
[10] #7 = true
|
||||
[11] #8 = StoreLocal Reassign unknown b$10 = unknown #7
|
||||
[12] Goto bb1
|
||||
bb1 (block)
|
||||
predecessors: bb2, bb3
|
||||
b$11: phi(bb2: b$9, bb3: b$10)
|
||||
[13] #10 = true
|
||||
[14] #11 = DeclareLocal Let unknown c$12
|
||||
[15] #16 = LoadLocal unknown props$5
|
||||
[16] If unknown #16 consequent=bb5 alternate=bb6 fallthrough=bb4
|
||||
bb5 (block)
|
||||
predecessors: bb1
|
||||
[17] #12 = true
|
||||
[18] #13 = StoreLocal Reassign unknown c$14 = unknown #12
|
||||
[19] Goto bb4
|
||||
bb6 (block)
|
||||
predecessors: bb1
|
||||
[20] #14 = 42
|
||||
[21] #15 = StoreLocal Reassign unknown c$15 = unknown #14
|
||||
[22] Goto bb4
|
||||
bb4 (block)
|
||||
predecessors: bb5, bb6
|
||||
c$16: phi(bb5: c$14, bb6: c$15)
|
||||
[23] #17 = LoadLocal unknown c$16
|
||||
[24] #18 = 42
|
||||
[25] #19 = 1
|
||||
[26] #20 = 43
|
||||
[27] #21 = 42
|
||||
[28] #22 = 1
|
||||
[29] #23 = 41
|
||||
[30] #24 = 42
|
||||
[31] #25 = 2
|
||||
[32] #26 = 84
|
||||
[33] #27 = 42
|
||||
[34] #28 = 2
|
||||
[35] #29 = 21
|
||||
[36] #30 = 0
|
||||
[37] #31 = 1
|
||||
[38] #32 = false
|
||||
[39] #33 = 0
|
||||
[40] #34 = 1
|
||||
[41] #35 = true
|
||||
[42] #36 = 0
|
||||
[43] #37 = 1
|
||||
[44] #38 = false
|
||||
[45] #39 = 0
|
||||
[46] #40 = 1
|
||||
[47] #41 = true
|
||||
[48] #42 = 0
|
||||
[49] #43 = 0
|
||||
[50] #44 = true
|
||||
[51] #45 = LoadGlobal NaN
|
||||
[52] #46 = LoadGlobal NaN
|
||||
[53] #47 = Binary unknown #45 == unknown #46
|
||||
[54] #48 = LoadGlobal NaN
|
||||
[55] #49 = LoadGlobal NaN
|
||||
[56] #50 = Binary unknown #48 != unknown #49
|
||||
[57] #51 = LoadGlobal NaN
|
||||
[58] #52 = LoadGlobal NaN
|
||||
[59] #53 = Binary unknown #51 !== unknown #52
|
||||
[60] #54 = LoadGlobal NaN
|
||||
[61] #55 = LoadGlobal NaN
|
||||
[62] #56 = Binary unknown #54 !== unknown #55
|
||||
[63] #57 = "hello"
|
||||
[64] #58 = "hello"
|
||||
[65] #59 = true
|
||||
[66] #60 = "hello"
|
||||
[67] #61 = "hello"
|
||||
[68] #62 = false
|
||||
[69] #63 = "hello"
|
||||
[70] #64 = "hello"
|
||||
[71] #65 = true
|
||||
[72] #66 = "hello"
|
||||
[73] #67 = "hello"
|
||||
[74] #68 = false
|
||||
[75] #69 = "hello"
|
||||
[76] #70 = "world"
|
||||
[77] #71 = false
|
||||
[78] #72 = "hello"
|
||||
[79] #73 = "world"
|
||||
[80] #74 = true
|
||||
[81] #75 = "hello"
|
||||
[82] #76 = "world"
|
||||
[83] #77 = false
|
||||
[84] #78 = "hello"
|
||||
[85] #79 = "world"
|
||||
[86] #80 = true
|
||||
[87] #81 = true
|
||||
[88] #82 = true
|
||||
[89] #83 = true
|
||||
[90] #84 = true
|
||||
[91] #85 = true
|
||||
[92] #86 = false
|
||||
[93] #87 = true
|
||||
[94] #88 = true
|
||||
[95] #89 = true
|
||||
[96] #90 = true
|
||||
[97] #91 = true
|
||||
[98] #92 = false
|
||||
[99] #93 = 5
|
||||
[100] #94 = 60
|
||||
[101] #95 = 300
|
||||
[102] #96 = 60
|
||||
[103] #97 = 18000
|
||||
[104] #98 = 1000
|
||||
[105] #99 = 18000000
|
||||
[106] #100 = StoreLocal Let unknown x$17 = unknown #99
|
||||
[107] #101 = 18000000
|
||||
[108] #102 = <undefined>
|
||||
[109] Return unknown #102
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ bb0 (block)
|
||||
[3] #3 = StoreLocal Let unknown y$5 = unknown #2
|
||||
[4] #4 = false
|
||||
[5] #5 = StoreLocal Reassign unknown y$6 = unknown #4
|
||||
[6] #6 = LoadLocal unknown y$6
|
||||
[6] #6 = false
|
||||
[7] #7 = DeclareLocal Let unknown z$7
|
||||
[8] #8 = LoadLocal unknown z$7
|
||||
[9] #9 = LoadLocal unknown x$4
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ bb1 (block)
|
||||
x$14: phi(bb3: x$10, bb6: x$13)
|
||||
z$18: phi(bb3: z$19, bb6: z$9)
|
||||
[21] #15 = LoadLocal unknown x$14
|
||||
[22] #16 = LoadLocal unknown y$8
|
||||
[22] #16 = 0
|
||||
[23] #17 = Binary unknown #15 + unknown #16
|
||||
[24] #18 = LoadLocal unknown z$18
|
||||
[25] #19 = Binary unknown #17 + unknown #18
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "hir-optimization"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
estree = { path = "../estree" }
|
||||
hir = { path = "../hir" }
|
||||
hir-ssa = { path = "../hir-ssa" }
|
||||
build-hir = { path = "../build-hir" }
|
||||
utils = { path = "../utils" }
|
||||
bumpalo = "3.13.0"
|
||||
indexmap = "2.0.0"
|
||||
miette = { version = "5.9.0" }
|
||||
thiserror = "1.0.41"
|
||||
@@ -0,0 +1,258 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use build_hir::initialize_hir;
|
||||
use estree::BinaryOperator;
|
||||
use hir::{
|
||||
BlockKind, Environment, Function, GotoKind, IdentifierId, Instruction, InstructionValue,
|
||||
LoadGlobal, Operand, Primitive, PrimitiveValue, TerminalValue,
|
||||
};
|
||||
use hir_ssa::eliminate_redundant_phis;
|
||||
|
||||
pub fn constant_propagation<'a>(env: &Environment<'a>, fun: &mut Function<'a>) {
|
||||
let mut constants = Constants::new();
|
||||
let have_terminals_changed = apply_constant_propagation(env, fun, &mut constants);
|
||||
if have_terminals_changed {
|
||||
// If terminals have changed then blocks may have become newly unreachable,
|
||||
// so reinitialize the HIR
|
||||
// TODO handle errors
|
||||
initialize_hir(&mut fun.body).unwrap();
|
||||
|
||||
// Now that predecessors have changed, prune phi operands for unreachable blocks
|
||||
// for example, a phi node whose operand was eliminated because it was set in a
|
||||
// block that is no longer reached
|
||||
for (_, block) in fun.body.blocks.iter_mut() {
|
||||
// TODO: avoid the clone here
|
||||
let predecessors = block.predecessors.clone();
|
||||
for phi in block.phis.iter_mut() {
|
||||
phi.operands
|
||||
.retain(|predecessor, _| predecessors.contains(predecessor))
|
||||
}
|
||||
}
|
||||
|
||||
// By removing some phi operands, there may be phis that were not previously
|
||||
// redundant but now are
|
||||
eliminate_redundant_phis(env, fun);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_constant_propagation<'a>(
|
||||
env: &Environment<'a>,
|
||||
fun: &mut Function<'a>,
|
||||
constants: &mut Constants<'a>,
|
||||
) -> bool {
|
||||
let mut has_changes = false;
|
||||
|
||||
for (_, block) in fun.body.blocks.iter_mut() {
|
||||
for phi in block.phis.iter() {
|
||||
let mut value: Option<Constant<'a>> = None;
|
||||
for (_, operand) in &phi.operands {
|
||||
if let Some(operand_value) = constants.get(&operand.id) {
|
||||
match &mut value {
|
||||
Some(value) if value == operand_value => {
|
||||
// no-op
|
||||
}
|
||||
Some(_) => {
|
||||
value = None;
|
||||
break;
|
||||
}
|
||||
None => {
|
||||
value = Some(operand_value.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This phi operand's value is unknown, bail out of replacing it
|
||||
value = None;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(value) = value {
|
||||
constants.insert(phi.identifier.id, value);
|
||||
}
|
||||
}
|
||||
for (ix, instr_ix) in block.instructions.iter().enumerate() {
|
||||
if block.kind == BlockKind::Sequence && ix == block.instructions.len() - 1 {
|
||||
// Evaluating the last value of a sequence can break order of evaluation
|
||||
// so skip these instructions
|
||||
continue;
|
||||
}
|
||||
let instr_ix = usize::from(*instr_ix);
|
||||
let mut instr = std::mem::replace(
|
||||
&mut fun.body.instructions[instr_ix].value,
|
||||
InstructionValue::Tombstone,
|
||||
);
|
||||
evaluate_instruction(env, &fun.body.instructions, &mut instr, constants);
|
||||
fun.body.instructions[instr_ix].value = instr;
|
||||
}
|
||||
|
||||
if block.kind != BlockKind::Block {
|
||||
// can't rewrite terminals in value blocks yet
|
||||
continue;
|
||||
}
|
||||
|
||||
if let TerminalValue::If(terminal) = &mut block.terminal.value {
|
||||
if let Some(primitive) =
|
||||
read_primitive_instruction(&fun.body.instructions, &terminal.test)
|
||||
{
|
||||
let target_block_id = if primitive.value.is_truthy() {
|
||||
terminal.consequent
|
||||
} else {
|
||||
terminal.alternate
|
||||
};
|
||||
block.terminal.value = TerminalValue::Goto(hir::GotoTerminal {
|
||||
block: target_block_id,
|
||||
kind: GotoKind::Break,
|
||||
});
|
||||
has_changes = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
has_changes
|
||||
}
|
||||
|
||||
fn read_primitive_instruction<'a>(
|
||||
instrs: &[Instruction<'a>],
|
||||
operand: &Operand,
|
||||
) -> Option<Primitive<'a>> {
|
||||
let instr = &instrs[usize::from(operand.ix)].value;
|
||||
if let InstructionValue::Primitive(primitive) = instr {
|
||||
Some(primitive.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_instruction<'a>(
|
||||
env: &Environment<'a>,
|
||||
instrs: &[Instruction<'a>],
|
||||
mut instr: &mut InstructionValue<'a>,
|
||||
constants: &mut Constants<'a>,
|
||||
) {
|
||||
let read_constant = |operand: &Operand| {
|
||||
let instr = &instrs[usize::from(operand.ix)].value;
|
||||
match instr {
|
||||
InstructionValue::Primitive(value) => Some(Constant::Primitive(value.clone())),
|
||||
InstructionValue::LoadGlobal(value) => Some(Constant::Global(value.clone())),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
match &mut instr {
|
||||
InstructionValue::Binary(value) => {
|
||||
let left = read_primitive_instruction(instrs, &value.left);
|
||||
let right = read_primitive_instruction(instrs, &value.right);
|
||||
match (left, right) {
|
||||
(Some(left), Some(right)) => {
|
||||
if let Some(result) = apply_binary_operator(env, left, value.operator, right) {
|
||||
*instr = InstructionValue::Primitive(result);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// no-op, not all operands are known
|
||||
}
|
||||
}
|
||||
}
|
||||
InstructionValue::LoadLocal(value) => {
|
||||
if let Some(const_value) = constants.get(&value.place.identifier.id) {
|
||||
*instr = const_value.into();
|
||||
}
|
||||
}
|
||||
InstructionValue::StoreLocal(value) => {
|
||||
if let Some(const_value) = read_constant(&value.value) {
|
||||
constants.insert(value.lvalue.identifier.identifier.id, const_value);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// no-op, not all instructions can be processed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_binary_operator<'a>(
|
||||
env: &Environment<'a>,
|
||||
left: Primitive<'a>,
|
||||
operator: BinaryOperator,
|
||||
right: Primitive<'a>,
|
||||
) -> Option<Primitive<'a>> {
|
||||
match (left.value, right.value) {
|
||||
(PrimitiveValue::Number(left), PrimitiveValue::Number(right)) => match operator {
|
||||
BinaryOperator::Add => Some(Primitive {
|
||||
value: PrimitiveValue::Number(left + right),
|
||||
}),
|
||||
BinaryOperator::Subtract => Some(Primitive {
|
||||
value: PrimitiveValue::Number(left - right),
|
||||
}),
|
||||
BinaryOperator::Multiply => Some(Primitive {
|
||||
value: PrimitiveValue::Number(left * right),
|
||||
}),
|
||||
BinaryOperator::Divide => Some(Primitive {
|
||||
value: PrimitiveValue::Number(left / right),
|
||||
}),
|
||||
BinaryOperator::LessThan => Some(Primitive {
|
||||
value: PrimitiveValue::Boolean(left < right),
|
||||
}),
|
||||
BinaryOperator::LessThanOrEqual => Some(Primitive {
|
||||
value: PrimitiveValue::Boolean(left <= right),
|
||||
}),
|
||||
BinaryOperator::GreaterThan => Some(Primitive {
|
||||
value: PrimitiveValue::Boolean(left > right),
|
||||
}),
|
||||
BinaryOperator::GreaterThanOrEqual => Some(Primitive {
|
||||
value: PrimitiveValue::Boolean(left >= right),
|
||||
}),
|
||||
BinaryOperator::Equals => Some(Primitive {
|
||||
value: PrimitiveValue::Boolean(left.equals(right)),
|
||||
}),
|
||||
BinaryOperator::NotEquals => Some(Primitive {
|
||||
value: PrimitiveValue::Boolean(left.not_equals(right)),
|
||||
}),
|
||||
BinaryOperator::StrictEquals => Some(Primitive {
|
||||
value: PrimitiveValue::Boolean(left.equals(right)),
|
||||
}),
|
||||
BinaryOperator::NotStrictEquals => Some(Primitive {
|
||||
value: PrimitiveValue::Boolean(left.not_equals(right)),
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
(left, right) => match operator {
|
||||
BinaryOperator::Equals => left.loosely_equals(&right).map(|value| Primitive {
|
||||
value: PrimitiveValue::Boolean(value),
|
||||
}),
|
||||
BinaryOperator::NotEquals => left.not_loosely_equals(&right).map(|value| Primitive {
|
||||
value: PrimitiveValue::Boolean(value),
|
||||
}),
|
||||
BinaryOperator::StrictEquals => Some(Primitive {
|
||||
value: PrimitiveValue::Boolean(left.strictly_equals(&right)),
|
||||
}),
|
||||
BinaryOperator::NotStrictEquals => Some(Primitive {
|
||||
value: PrimitiveValue::Boolean(left.not_strictly_equals(&right)),
|
||||
}),
|
||||
_ => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Constants<'a> = HashMap<IdentifierId, Constant<'a>>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum Constant<'a> {
|
||||
Global(LoadGlobal<'a>),
|
||||
Primitive(Primitive<'a>),
|
||||
}
|
||||
|
||||
impl<'a> From<&Constant<'a>> for InstructionValue<'a> {
|
||||
fn from(value: &Constant<'a>) -> Self {
|
||||
match value {
|
||||
Constant::Global(value) => InstructionValue::LoadGlobal(value.clone()),
|
||||
Constant::Primitive(value) => InstructionValue::Primitive(value.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Constant<'a>> for InstructionValue<'a> {
|
||||
fn from(value: Constant<'a>) -> Self {
|
||||
match value {
|
||||
Constant::Global(value) => InstructionValue::LoadGlobal(value),
|
||||
Constant::Primitive(value) => InstructionValue::Primitive(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod constant_propagation;
|
||||
|
||||
pub use constant_propagation::constant_propagation;
|
||||
@@ -16,7 +16,7 @@ use utils::RetainMut;
|
||||
/// and phis rewrite all their identifiers based on this table. The algorithm loops over the CFG repeatedly
|
||||
/// until there are no new rewrites: for a CFG without back-edges it completes in a single pass.
|
||||
type Rewrites<'a> = HashMap<IdentifierId, Identifier<'a>>;
|
||||
pub fn eliminate_redundant_phis<'a>(_env: &'a Environment, fun: &mut Function<'a>) {
|
||||
pub fn eliminate_redundant_phis<'a>(_env: &Environment, fun: &mut Function<'a>) {
|
||||
let hir = &mut fun.body;
|
||||
let mut rewrites = Rewrites::new();
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ impl<'a> Instruction<'a> {
|
||||
InstructionValue::StoreLocal(instr) => {
|
||||
f(&mut instr.lvalue);
|
||||
}
|
||||
InstructionValue::Tombstone => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +50,7 @@ impl<'a> Instruction<'a> {
|
||||
InstructionValue::LoadLocal(instr) => f(&mut instr.place),
|
||||
InstructionValue::Primitive(_) => {}
|
||||
InstructionValue::StoreLocal(_) => {}
|
||||
InstructionValue::Tombstone => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +89,7 @@ pub enum InstructionValue<'a> {
|
||||
// Template(Template<'a>),
|
||||
// TypeCast(TypeCast<'a>),
|
||||
// Unary(Unary<'a>),
|
||||
// Unsupported(Unsupported<'a>),
|
||||
Tombstone,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -108,12 +110,12 @@ pub struct Binary {
|
||||
pub right: Operand,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Primitive<'a> {
|
||||
pub value: PrimitiveValue<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PrimitiveValue<'a> {
|
||||
Boolean(bool),
|
||||
Null,
|
||||
@@ -122,20 +124,140 @@ pub enum PrimitiveValue<'a> {
|
||||
Undefined,
|
||||
}
|
||||
|
||||
impl<'a> PrimitiveValue<'a> {
|
||||
pub fn is_truthy(&self) -> bool {
|
||||
match &self {
|
||||
PrimitiveValue::Boolean(value) => *value,
|
||||
PrimitiveValue::Number(value) => value.is_truthy(),
|
||||
PrimitiveValue::String(value) => value.len() != 0,
|
||||
PrimitiveValue::Null => false,
|
||||
PrimitiveValue::Undefined => false,
|
||||
}
|
||||
}
|
||||
|
||||
// Partial implementation of loose equality for javascript, returns Some for supported
|
||||
// cases w the equality result, and None for unsupported cases
|
||||
pub fn loosely_equals(&self, other: &Self) -> Option<bool> {
|
||||
// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islooselyequal
|
||||
match (&self, &other) {
|
||||
// 1. If Type(x) is Type(y), then
|
||||
// a. Return IsStrictlyEqual(x, y).
|
||||
(PrimitiveValue::Number(left), PrimitiveValue::Number(right)) => {
|
||||
Some(left.equals(*right))
|
||||
}
|
||||
(PrimitiveValue::Null, PrimitiveValue::Null) => Some(true),
|
||||
(PrimitiveValue::Undefined, PrimitiveValue::Undefined) => Some(true),
|
||||
(PrimitiveValue::Boolean(left), PrimitiveValue::Boolean(right)) => Some(left == right),
|
||||
(PrimitiveValue::String(left), PrimitiveValue::String(right)) => Some(left == right),
|
||||
|
||||
// 2. If x is null and y is undefined, return true.
|
||||
(PrimitiveValue::Null, PrimitiveValue::Undefined) => Some(true),
|
||||
|
||||
// 3. If x is undefined and y is null, return true.
|
||||
(PrimitiveValue::Undefined, PrimitiveValue::Null) => Some(true),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_loosely_equals(&self, other: &Self) -> Option<bool> {
|
||||
self.loosely_equals(other).map(|value| !value)
|
||||
}
|
||||
|
||||
// Complete implementation of strict equality for javascript
|
||||
pub fn strictly_equals(&self, other: &Self) -> bool {
|
||||
// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-isstrictlyequal
|
||||
match (&self, &other) {
|
||||
(PrimitiveValue::Number(left), PrimitiveValue::Number(right)) => left.equals(*right),
|
||||
(PrimitiveValue::Null, PrimitiveValue::Null) => true,
|
||||
(PrimitiveValue::Undefined, PrimitiveValue::Undefined) => true,
|
||||
(PrimitiveValue::Boolean(left), PrimitiveValue::Boolean(right)) => left == right,
|
||||
(PrimitiveValue::String(left), PrimitiveValue::String(right)) => left == right,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_strictly_equals(&self, other: &Self) -> bool {
|
||||
!self.strictly_equals(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a JavaScript Number as its binary representation so that
|
||||
/// -1 == -1, NaN == Nan etc.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
|
||||
/// Note: NaN is *always* represented as the f64::NAN constant to allow
|
||||
/// comparison of NaNs.
|
||||
#[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Debug, Hash)]
|
||||
pub struct Number(u64);
|
||||
|
||||
impl From<f64> for Number {
|
||||
fn from(value: f64) -> Self {
|
||||
Self(value.to_bits())
|
||||
if value.is_nan() {
|
||||
Self(f64::NAN.to_bits())
|
||||
} else {
|
||||
Self(value.to_bits())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Number> for f64 {
|
||||
fn from(value: Number) -> Self {
|
||||
f64::from_bits(value.0)
|
||||
fn from(number: Number) -> Self {
|
||||
let value = f64::from_bits(number.0);
|
||||
assert!(!f64::is_nan(value) || number.0 == f64::NAN.to_bits());
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
impl Number {
|
||||
pub fn equals(self, other: Self) -> bool {
|
||||
f64::from(self) == f64::from(other)
|
||||
}
|
||||
|
||||
pub fn not_equals(self, other: Self) -> bool {
|
||||
!self.equals(other)
|
||||
}
|
||||
|
||||
pub fn is_truthy(self) -> bool {
|
||||
let value = f64::from(self);
|
||||
if self.0 == f64::NAN.to_bits() || value == 0.0 || value == -0.0 {
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Number {
|
||||
type Output = Number;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
let result = f64::from(self) + f64::from(rhs);
|
||||
Self::from(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Sub for Number {
|
||||
type Output = Number;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
let result = f64::from(self) - f64::from(rhs);
|
||||
Self::from(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul for Number {
|
||||
type Output = Number;
|
||||
|
||||
fn mul(self, rhs: Self) -> Self::Output {
|
||||
let result = f64::from(self) * f64::from(rhs);
|
||||
Self::from(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div for Number {
|
||||
type Output = Number;
|
||||
|
||||
fn div(self, rhs: Self) -> Self::Output {
|
||||
let result = f64::from(self) / f64::from(rhs);
|
||||
Self::from(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +271,7 @@ pub struct LoadContext {
|
||||
pub place: Operand,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct LoadGlobal<'a> {
|
||||
pub name: String<'a>,
|
||||
}
|
||||
|
||||
@@ -146,6 +146,9 @@ impl<'a> Print<'a> for InstructionValue<'a> {
|
||||
write!(out, " {} ", value.operator)?;
|
||||
value.right.print(hir, out)?;
|
||||
}
|
||||
InstructionValue::Tombstone => {
|
||||
write!(out, "Tombstone!")?;
|
||||
}
|
||||
_ => write!(out, "{:?}", self)?,
|
||||
}
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user