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(())