From 8ada08f11e218f1a33da28a968bfb700c2abc40d Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 12 Jul 2023 17:33:54 +0900 Subject: [PATCH] [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. --- compiler/forget/Cargo.lock | 16 ++ compiler/forget/Cargo.toml | 1 + .../forget/crates/build-hir/src/builder.rs | 32 ++- compiler/forget/crates/build-hir/src/lib.rs | 5 + compiler/forget/crates/estree-swc/src/lib.rs | 2 +- compiler/forget/crates/fixtures/Cargo.toml | 1 + ...stant-propagation-constant-if-condition.js | 17 ++ .../tests/fixtures/constant-propagation.js | 60 ++++ .../crates/fixtures/tests/fixtures_test.rs | 3 + ...-propagation-constant-if-condition.js.snap | 39 +++ ...est__fixtures@constant-propagation.js.snap | 199 ++++++++++++++ ...ixtures_test__fixtures@identifiers.js.snap | 2 +- ...res_test__fixtures@ssa-reassign-if.js.snap | 2 +- .../forget/crates/hir-optimization/Cargo.toml | 17 ++ .../src/constant_propagation.rs | 258 ++++++++++++++++++ .../forget/crates/hir-optimization/src/lib.rs | 3 + .../hir-ssa/src/eliminate_redundant_phis.rs | 2 +- compiler/forget/crates/hir/src/instruction.rs | 138 +++++++++- compiler/forget/crates/hir/src/print.rs | 3 + 19 files changed, 774 insertions(+), 26 deletions(-) create mode 100644 compiler/forget/crates/fixtures/tests/fixtures/constant-propagation-constant-if-condition.js create mode 100644 compiler/forget/crates/fixtures/tests/fixtures/constant-propagation.js create mode 100644 compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@constant-propagation-constant-if-condition.js.snap create mode 100644 compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@constant-propagation.js.snap create mode 100644 compiler/forget/crates/hir-optimization/Cargo.toml create mode 100644 compiler/forget/crates/hir-optimization/src/constant_propagation.rs create mode 100644 compiler/forget/crates/hir-optimization/src/lib.rs diff --git a/compiler/forget/Cargo.lock b/compiler/forget/Cargo.lock index 56b8310dea..0e15da8ffc 100644 --- a/compiler/forget/Cargo.lock +++ b/compiler/forget/Cargo.lock @@ -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" diff --git a/compiler/forget/Cargo.toml b/compiler/forget/Cargo.toml index 42a921df89..ea9ab66e99 100644 --- a/compiler/forget/Cargo.toml +++ b/compiler/forget/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/fixtures", "crates/hir", "crates/hir-ssa", + "crates/hir-optimization", "crates/swc-demo", "crates/estree", "crates/estree-codegen", diff --git a/compiler/forget/crates/build-hir/src/builder.rs b/compiler/forget/crates/build-hir/src/builder.rs index 82534bdbbd..1907280393 100644 --- a/compiler/forget/crates/build-hir/src/builder.rs +++ b/compiler/forget/crates/build-hir/src/builder.rs @@ -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::::with_capacity(hir.blocks.len()); let mut postorder = std::vec::Vec::::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 = 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 = 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 = 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(); } diff --git a/compiler/forget/crates/build-hir/src/lib.rs b/compiler/forget/crates/build-hir/src/lib.rs index 15fd7765c5..6d102bed46 100644 --- a/compiler/forget/crates/build-hir/src/lib.rs +++ b/compiler/forget/crates/build-hir/src/lib.rs @@ -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::*; diff --git a/compiler/forget/crates/estree-swc/src/lib.rs b/compiler/forget/crates/estree-swc/src/lib.rs index f90d3eb8c2..d69050a41e 100644 --- a/compiler/forget/crates/estree-swc/src/lib.rs +++ b/compiler/forget/crates/estree-swc/src/lib.rs @@ -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), diff --git a/compiler/forget/crates/fixtures/Cargo.toml b/compiler/forget/crates/fixtures/Cargo.toml index 3a3f140bbb..45cea2d32f 100644 --- a/compiler/forget/crates/fixtures/Cargo.toml +++ b/compiler/forget/crates/fixtures/Cargo.toml @@ -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"] } diff --git a/compiler/forget/crates/fixtures/tests/fixtures/constant-propagation-constant-if-condition.js b/compiler/forget/crates/fixtures/tests/fixtures/constant-propagation-constant-if-condition.js new file mode 100644 index 0000000000..3e6f4cdf19 --- /dev/null +++ b/compiler/forget/crates/fixtures/tests/fixtures/constant-propagation-constant-if-condition.js @@ -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; +} diff --git a/compiler/forget/crates/fixtures/tests/fixtures/constant-propagation.js b/compiler/forget/crates/fixtures/tests/fixtures/constant-propagation.js new file mode 100644 index 0000000000..c7500c103b --- /dev/null +++ b/compiler/forget/crates/fixtures/tests/fixtures/constant-propagation.js @@ -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; // + + // 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; +} diff --git a/compiler/forget/crates/fixtures/tests/fixtures_test.rs b/compiler/forget/crates/fixtures/tests/fixtures_test.rs index 24a3132045..1471390e8c 100644 --- a/compiler/forget/crates/fixtures/tests/fixtures_test.rs +++ b/compiler/forget/crates/fixtures/tests/fixtures_test.rs @@ -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) => { diff --git a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@constant-propagation-constant-if-condition.js.snap b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@constant-propagation-constant-if-condition.js.snap new file mode 100644 index 0000000000..178a366286 --- /dev/null +++ b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@constant-propagation-constant-if-condition.js.snap @@ -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 + diff --git a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@constant-propagation.js.snap b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@constant-propagation.js.snap new file mode 100644 index 0000000000..c34ee5b362 --- /dev/null +++ b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@constant-propagation.js.snap @@ -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; // + + // 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 = + [109] Return unknown #102 + diff --git a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@identifiers.js.snap b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@identifiers.js.snap index 86bfdd1c56..67c5144c27 100644 --- a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@identifiers.js.snap +++ b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@identifiers.js.snap @@ -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 diff --git a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@ssa-reassign-if.js.snap b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@ssa-reassign-if.js.snap index a48c570161..344936f57c 100644 --- a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@ssa-reassign-if.js.snap +++ b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@ssa-reassign-if.js.snap @@ -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 diff --git a/compiler/forget/crates/hir-optimization/Cargo.toml b/compiler/forget/crates/hir-optimization/Cargo.toml new file mode 100644 index 0000000000..64b96662cd --- /dev/null +++ b/compiler/forget/crates/hir-optimization/Cargo.toml @@ -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" \ No newline at end of file diff --git a/compiler/forget/crates/hir-optimization/src/constant_propagation.rs b/compiler/forget/crates/hir-optimization/src/constant_propagation.rs new file mode 100644 index 0000000000..a6b9c13c53 --- /dev/null +++ b/compiler/forget/crates/hir-optimization/src/constant_propagation.rs @@ -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> = 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> { + 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> { + 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>; + +#[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> for InstructionValue<'a> { + fn from(value: Constant<'a>) -> Self { + match value { + Constant::Global(value) => InstructionValue::LoadGlobal(value), + Constant::Primitive(value) => InstructionValue::Primitive(value), + } + } +} diff --git a/compiler/forget/crates/hir-optimization/src/lib.rs b/compiler/forget/crates/hir-optimization/src/lib.rs new file mode 100644 index 0000000000..96d2279b5c --- /dev/null +++ b/compiler/forget/crates/hir-optimization/src/lib.rs @@ -0,0 +1,3 @@ +mod constant_propagation; + +pub use constant_propagation::constant_propagation; diff --git a/compiler/forget/crates/hir-ssa/src/eliminate_redundant_phis.rs b/compiler/forget/crates/hir-ssa/src/eliminate_redundant_phis.rs index 50b06cbf69..2489792a64 100644 --- a/compiler/forget/crates/hir-ssa/src/eliminate_redundant_phis.rs +++ b/compiler/forget/crates/hir-ssa/src/eliminate_redundant_phis.rs @@ -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>; -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(); diff --git a/compiler/forget/crates/hir/src/instruction.rs b/compiler/forget/crates/hir/src/instruction.rs index b251f04368..8bf7ec0f56 100644 --- a/compiler/forget/crates/hir/src/instruction.rs +++ b/compiler/forget/crates/hir/src/instruction.rs @@ -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 { + // 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 { + 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 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 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>, } diff --git a/compiler/forget/crates/hir/src/print.rs b/compiler/forget/crates/hir/src/print.rs index 1a401a2ef4..6e30f9ab3f 100644 --- a/compiler/forget/crates/hir/src/print.rs +++ b/compiler/forget/crates/hir/src/print.rs @@ -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(())