From c7ccc5dd30acfb29c0e95e42ade5247514a2f270 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Wed, 12 Jul 2023 12:38:13 +0900 Subject: [PATCH] [rust] Initial port of EnterSSA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a first pass at porting EnterSSA to Rust. First pass in the sense that it's hard to fully test it, and also in the sense that we'll likely figure out even better ways to work w the HIR as we iterate. Oh and i didn't do recusing into function expressions yet, since we can't even represent function expressions yet, and that may require modifying the design a bit (though i have an idea that i think will work, which is for the Builder to have an optional parent. When we encounter a block with no predecessors, we check the parent. I _think_ this will make all the borrowing "just work"). A few notes: Rust's compilation model parallelizes and incrementally computes at the crate granularity, so builds are faster if we split up our code into more but smaller crates. Setting up a clean dependency graph can dramatically improve build performance too. For example, to run the `fixtures` tests we can build `estree` and `hir` in parallel, then once those build _all_ of our other crates can be built in parallel until we get to `fixtures` which depends on the everything else. The various passes don't have any build dependencies on each other so they can build in parallel. Hence the new code for SSA stuff is in a separate `hir-ssa` crate. We should similarly group other passes (approximately one crate per folder in the babel-plugin-react-forget/src/ directory, eg SSA, Inference, Optimization, etc). Second, shared mutable ownership can be modeled in Rust but requires wrappers such as `Rc>`. It's generally more efficient and more idiomatic to rethink the data model and algorithm. For EnterSSA, the Builder object holds a reference into the HIR that it only ever reads, and the pass (which drives the builder) also holds a reference into the HIR, which it mutates. The previous PR split up Blocks and Instructions, and the value of that is more apparent in `enter_ssa()`. The Rust equivalent of the builder holds a _shared_ (immutable) reference to just the HIR's blocks, while the pass (driving the builder) holds a _unique_ (mutable) reference to just the HIR's instructions. This lets us keep the overall feel of the algorithm while keeping Rust happy. Also note that the other change — to making operands be InstrIx indices into the instructions array — means that the SSA logic is simpler. Most instructions don't have to be visited at all, since they don't deal with loads/stores. Terminals also don't need to be visited, since they reference instructions, not identifiers. The Phi concept seems to just work too. I also updated the printer to print predecessors and phis. --- compiler/forget/crates/build-hir/src/build.rs | 40 ++- .../forget/crates/build-hir/src/builder.rs | 2 + compiler/forget/crates/fixtures/Cargo.toml | 1 + .../tests/fixtures/ssa-reassign-if.js | 16 ++ .../crates/fixtures/tests/fixtures_test.rs | 6 +- ...tures_test__fixtures@for-statement.js.snap | 39 ++- ...ixtures_test__fixtures@identifiers.js.snap | 17 +- ...xtures_test__fixtures@if-statement.js.snap | 25 +- .../fixtures_test__fixtures@simple.js.snap | 4 +- ...res_test__fixtures@ssa-reassign-if.js.snap | 77 +++++ compiler/forget/crates/hir-ssa/Cargo.toml | 1 + compiler/forget/crates/hir-ssa/src/enter.rs | 262 ++++++++++++++++-- compiler/forget/crates/hir/src/basic_block.rs | 27 +- compiler/forget/crates/hir/src/function.rs | 15 +- compiler/forget/crates/hir/src/instruction.rs | 42 +++ compiler/forget/crates/hir/src/print.rs | 49 +++- compiler/forget/crates/hir/src/types.rs | 4 +- 17 files changed, 548 insertions(+), 79 deletions(-) create mode 100644 compiler/forget/crates/fixtures/tests/fixtures/ssa-reassign-if.js create mode 100644 compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@ssa-reassign-if.js.snap diff --git a/compiler/forget/crates/build-hir/src/build.rs b/compiler/forget/crates/build-hir/src/build.rs index c6f7d9e148..4b9e7bb2fb 100644 --- a/compiler/forget/crates/build-hir/src/build.rs +++ b/compiler/forget/crates/build-hir/src/build.rs @@ -6,8 +6,8 @@ use estree::{ }; use hir::{ ArrayElement, BlockKind, BranchTerminal, Environment, ForTerminal, Function, GotoKind, - Identifier, IdentifierOperand, InstrIx, InstructionKind, InstructionValue, LValue, LoadGlobal, - LoadLocal, Operand, PrimitiveValue, TerminalValue, + IdentifierOperand, InstrIx, InstructionKind, InstructionValue, LValue, LoadGlobal, LoadLocal, + Operand, PrimitiveValue, TerminalValue, }; use crate::{ @@ -23,17 +23,17 @@ use crate::{ /// Failures generally include nonsensical input (`delete 1`) or syntax /// that is not yet supported. pub fn build<'a>( - environment: &'a Environment<'a>, + env: &'a Environment<'a>, fun: FunctionDeclaration, -) -> Result, BuildDiagnostic> { - let mut builder = Builder::new(environment); +) -> Result<&'a mut Function<'a>, BuildDiagnostic> { + let mut builder = Builder::new(env); match fun.function.body { Some(estree::FunctionBody::BlockStatement(body)) => { - lower_block_statement(environment, &mut builder, *body, None)? + lower_block_statement(env, &mut builder, *body, None)? } Some(estree::FunctionBody::Expression(body)) => { - lower_expression(environment, &mut builder, body)?; + lower_expression(env, &mut builder, body)?; } None => { return Err(BuildDiagnostic::new( @@ -44,11 +44,26 @@ pub fn build<'a>( } } + let mut params = Vec::with_capacity_in(fun.function.params.len(), &env.allocator); + for param in fun.function.params { + match param { + Pattern::Identifier(param) => { + let identifier = lower_identifier_for_assignment( + env, + &mut builder, + InstructionKind::Let, + *param, + )?; + params.push(identifier); + } + } + } + // In case the function did not explicitly return, terminate the final // block with an explicit `return undefined`. If the function *did* return, // this will be unreachable and get pruned later. let implicit_return_value = lower_value_to_temporary( - environment, + env, &mut builder, InstructionValue::Primitive(hir::Primitive { value: PrimitiveValue::Undefined, @@ -65,11 +80,16 @@ pub fn build<'a>( ); let body = builder.build()?; - Ok(Function { + Ok(env.alloc(Function { + id: fun + .function + .id + .map(|id| String::from_str_in(&id.name, &env.allocator)), body, + params, is_async: fun.function.is_async, is_generator: fun.function.is_generator, - }) + })) } fn lower_block_statement<'a>( diff --git a/compiler/forget/crates/build-hir/src/builder.rs b/compiler/forget/crates/build-hir/src/builder.rs index 0d50ee9907..1a09348a1c 100644 --- a/compiler/forget/crates/build-hir/src/builder.rs +++ b/compiler/forget/crates/build-hir/src/builder.rs @@ -171,6 +171,7 @@ impl<'a> Builder<'a> { value: terminal, }, predecessors: Default::default(), + phis: Vec::new_in(&self.environment.allocator), }, &self.environment.allocator, ), @@ -226,6 +227,7 @@ impl<'a> Builder<'a> { value: terminal, }, predecessors: Default::default(), + phis: Vec::new_in(&self.environment.allocator), }, &self.environment.allocator, ), diff --git a/compiler/forget/crates/fixtures/Cargo.toml b/compiler/forget/crates/fixtures/Cargo.toml index 2c5f4a0e0d..3a3f140bbb 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-ssa = { path = "../hir-ssa" } build-hir = { path = "../build-hir" } bumpalo = { version = "3.13.0", features = ["collections"] } miette = { version = "5.9.0", features = ["backtrace", "fancy"] } diff --git a/compiler/forget/crates/fixtures/tests/fixtures/ssa-reassign-if.js b/compiler/forget/crates/fixtures/tests/fixtures/ssa-reassign-if.js new file mode 100644 index 0000000000..6c8029f840 --- /dev/null +++ b/compiler/forget/crates/fixtures/tests/fixtures/ssa-reassign-if.js @@ -0,0 +1,16 @@ +function Component(a, b) { + let x; + let y = 0; + let z = 10; + if (a) { + x = 1; + if (b) { + z = 20; + } else { + z = 30; + } + } else { + x = 2; + } + return x + y + z; +} diff --git a/compiler/forget/crates/fixtures/tests/fixtures_test.rs b/compiler/forget/crates/fixtures/tests/fixtures_test.rs index 1a96f13d54..adcbcb1265 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_ssa::enter_ssa; use insta::{assert_snapshot, glob}; use miette::{NamedSource, Report}; @@ -31,8 +32,9 @@ fn fixtures() { output.push_str("\n\n"); } match build(&environment, *fun) { - Ok(hir) => { - hir.print(&hir.body, &mut output).unwrap(); + Ok(mut fun) => { + enter_ssa(&environment, &mut fun).unwrap(); + fun.print(&fun.body, &mut output).unwrap(); } Err(error) => { write!(&mut output, "{}", error,).unwrap(); diff --git a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@for-statement.js.snap b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@for-statement.js.snap index dc47e85f19..00a1d66649 100644 --- a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@for-statement.js.snap +++ b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@for-statement.js.snap @@ -14,33 +14,42 @@ function foo() { Output: +function foo( +) entry bb0 -bb0 +bb0 (block) [0] #0 = 0 - [1] #1 = StoreLocal Let unknown x$0 = unknown #0 + [1] #1 = StoreLocal Let unknown x$2 = unknown #0 [2] For init=bb3 test=bb1 update=bb4 body=bb5 fallthrough=bb2 -bb3 +bb3 (loop) + predecessors: bb0 [3] #2 = 0 - [4] #3 = StoreLocal Let unknown i$1 = unknown #2 + [4] #3 = StoreLocal Let unknown i$3 = unknown #2 [5] Goto bb1 -bb1 - [6] #12 = LoadLocal unknown i$1 +bb1 (loop) + predecessors: bb3, bb4 + i$4: phi(bb3: i$3, bb4: i$7) + x$5: phi(bb3: x$2, bb4: x$6) + [6] #12 = LoadLocal unknown i$4 [7] #13 = 10 [8] #14 = Binary unknown #12 < unknown #13 [9] Branch unknown #14 consequent=bb5 alternate=bb2 -bb5 - [10] #8 = LoadLocal unknown x$0 - [11] #9 = LoadLocal unknown i$1 +bb5 (block) + predecessors: bb1 + [10] #8 = LoadLocal unknown x$5 + [11] #9 = LoadLocal unknown i$4 [12] #10 = Binary unknown #8 + unknown #9 - [13] #11 = StoreLocal Reassign unknown x$0 = unknown #10 + [13] #11 = StoreLocal Reassign unknown x$6 = unknown #10 [14] Goto bb4 -bb4 - [15] #4 = LoadLocal unknown i$1 +bb4 (loop) + predecessors: bb5 + [15] #4 = LoadLocal unknown i$4 [16] #5 = 1 [17] #6 = Binary unknown #4 + unknown #5 - [18] #7 = StoreLocal Reassign unknown i$1 = unknown #6 + [18] #7 = StoreLocal Reassign unknown i$7 = unknown #6 [19] Goto bb1 -bb2 - [20] #15 = LoadLocal unknown x$0 +bb2 (block) + predecessors: bb1 + [20] #15 = LoadLocal unknown x$5 [21] Return unknown #15 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 b9b9018942..86bfdd1c56 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 @@ -23,17 +23,20 @@ function id(x) { Output: +function id( + unknown x$4, +) entry bb0 -bb0 +bb0 (block) [0] #0 = LoadGlobal Math [1] #1 = LoadGlobal id [2] #2 = true - [3] #3 = StoreLocal Let unknown y$1 = unknown #2 + [3] #3 = StoreLocal Let unknown y$5 = unknown #2 [4] #4 = false - [5] #5 = StoreLocal Reassign unknown y$1 = unknown #4 - [6] #6 = LoadLocal unknown y$1 - [7] #7 = DeclareLocal Let unknown z$2 - [8] #8 = LoadLocal unknown z$2 - [9] #9 = LoadLocal unknown x$3 + [5] #5 = StoreLocal Reassign unknown y$6 = unknown #4 + [6] #6 = LoadLocal unknown y$6 + [7] #7 = DeclareLocal Let unknown z$7 + [8] #8 = LoadLocal unknown z$7 + [9] #9 = LoadLocal unknown x$4 [10] Return unknown #9 diff --git a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@if-statement.js.snap b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@if-statement.js.snap index 1a8a0e7910..5119fb1312 100644 --- a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@if-statement.js.snap +++ b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@if-statement.js.snap @@ -15,18 +15,27 @@ function foo(a, b, c, d) { Output: +function foo( + unknown a$4, + unknown b$5, + unknown c$6, + unknown d$7, +) entry bb0 -bb0 - [0] #2 = LoadLocal unknown a$2 +bb0 (block) + [0] #2 = LoadLocal unknown a$4 [1] If unknown #2 consequent=bb2 alternate=bb4 fallthrough=bb1 -bb2 - [2] #0 = LoadLocal unknown b$0 +bb2 (block) + predecessors: bb0 + [2] #0 = LoadLocal unknown b$5 [3] Return unknown #0 -bb4 - [4] #1 = LoadLocal unknown c$1 +bb4 (block) + predecessors: bb0 + [4] #1 = LoadLocal unknown c$6 [5] Goto bb1 -bb1 - [6] #3 = LoadLocal unknown d$3 +bb1 (block) + predecessors: bb4 + [6] #3 = LoadLocal unknown d$7 [7] #4 = [8] Return unknown #4 diff --git a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@simple.js.snap b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@simple.js.snap index 3360c12643..191330b66d 100644 --- a/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@simple.js.snap +++ b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@simple.js.snap @@ -11,8 +11,10 @@ function test() { Output: +function test( +) entry bb0 -bb0 +bb0 (block) [0] #0 = true [1] #1 = false [2] #2 = null 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 new file mode 100644 index 0000000000..b3d238e947 --- /dev/null +++ b/compiler/forget/crates/fixtures/tests/snapshots/fixtures_test__fixtures@ssa-reassign-if.js.snap @@ -0,0 +1,77 @@ +--- +source: crates/fixtures/tests/fixtures_test.rs +expression: "format!(\"Input:\\n{input}\\n\\nOutput:\\n{output}\")" +input_file: crates/fixtures/tests/fixtures/ssa-reassign-if.js +--- +Input: +function Component(a, b) { + let x; + let y = 0; + let z = 10; + if (a) { + x = 1; + if (b) { + z = 20; + } else { + z = 30; + } + } else { + x = 2; + } + return x + y + z; +} + + +Output: +function Component( + unknown a$5, + unknown b$6, +) +entry bb0 +bb0 (block) + [0] #0 = DeclareLocal Let unknown x$7 + [1] #1 = 0 + [2] #2 = StoreLocal Let unknown y$8 = unknown #1 + [3] #3 = 10 + [4] #4 = StoreLocal Let unknown z$9 = unknown #3 + [5] #14 = LoadLocal unknown a$5 + [6] If unknown #14 consequent=bb2 alternate=bb6 fallthrough=bb1 +bb2 (block) + predecessors: bb0 + [7] #5 = 1 + [8] #6 = StoreLocal Reassign unknown x$10 = unknown #5 + [9] #11 = LoadLocal unknown b$6 + [10] If unknown #11 consequent=bb4 alternate=bb5 fallthrough=bb3 +bb4 (block) + predecessors: bb2 + [11] #7 = 20 + [12] #8 = StoreLocal Reassign unknown z$11 = unknown #7 + [13] Goto bb3 +bb5 (block) + predecessors: bb2 + [14] #9 = 30 + [15] #10 = StoreLocal Reassign unknown z$12 = unknown #9 + [16] Goto bb3 +bb3 (block) + predecessors: bb4, bb5 + x$15: phi(bb4: x$10, bb5: x$10) + y$17: phi(bb4: y$8, bb5: y$8) + z$19: phi(bb4: z$11, bb5: z$12) + [17] Goto bb1 +bb6 (block) + predecessors: bb0 + [18] #12 = 2 + [19] #13 = StoreLocal Reassign unknown x$13 = unknown #12 + [20] Goto bb1 +bb1 (block) + predecessors: bb3, bb6 + x$14: phi(bb3: x$15, bb6: x$13) + y$16: phi(bb3: y$17, bb6: y$8) + z$18: phi(bb3: z$19, bb6: z$9) + [21] #15 = LoadLocal unknown x$14 + [22] #16 = LoadLocal unknown y$16 + [23] #17 = Binary unknown #15 + unknown #16 + [24] #18 = LoadLocal unknown z$18 + [25] #19 = Binary unknown #17 + unknown #18 + [26] Return unknown #19 + diff --git a/compiler/forget/crates/hir-ssa/Cargo.toml b/compiler/forget/crates/hir-ssa/Cargo.toml index fd011a2620..da0aa7f361 100644 --- a/compiler/forget/crates/hir-ssa/Cargo.toml +++ b/compiler/forget/crates/hir-ssa/Cargo.toml @@ -8,5 +8,6 @@ edition = "2021" [dependencies] hir = { path = "../hir" } 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-ssa/src/enter.rs b/compiler/forget/crates/hir-ssa/src/enter.rs index 96f29f3ded..8f45ea6e59 100644 --- a/compiler/forget/crates/hir-ssa/src/enter.rs +++ b/compiler/forget/crates/hir-ssa/src/enter.rs @@ -1,38 +1,248 @@ -use std::collections::HashSet; +use std::{cell::RefCell, rc::Rc}; -use hir::{BlockId, Environment, Function, HIR}; +use bumpalo::collections::{CollectIn, Vec}; +use hir::{ + BasicBlock, BlockId, Blocks, Environment, Function, Identifier, IdentifierData, IdentifierId, + IdentifierOperand, Instruction, LValue, MutableRange, Phi, HIR, +}; +use indexmap::{IndexMap, IndexSet}; use thiserror::Error; #[derive(Error, Debug)] #[error("Error constructing SSA form")] pub struct SSAError; -pub fn enter_ssa<'a>(env: &'a Environment<'a>, hir: &mut Function<'a>) -> Result<(), SSAError> { - let mut builder = Builder::new(env); - enter_ssa_impl(&mut builder, hir, hir.body.entry) -} - -struct Builder<'a> { - env: &'a Environment<'a>, -} - -impl<'a> Builder<'a> { - fn new(env: &'a Environment<'a>) -> Self { - Self { env } +pub fn enter_ssa<'a>(env: &Environment<'a>, fun: &mut Function<'a>) -> Result<(), SSAError> { + let blocks = &fun.body.blocks; + let instructions = &mut fun.body.instructions; + let mut builder = Builder::new(env, fun.body.entry, blocks); + for param in &mut fun.params { + builder.visit_param(param); } -} + enter_ssa_impl(&mut builder, instructions)?; -fn enter_ssa_impl<'a>( - builder: &mut Builder<'a>, - hir: &mut Function<'a>, - root_block_id: BlockId, -) -> Result<(), SSAError> { - let mut visited = HashSet::with_capacity(hir.body.blocks.len()); - for (block_id, block) in hir.body.blocks.iter_mut() { - if !visited.insert(*block_id) { - // Visited the same block twice - return Err(SSAError); - } + let mut states = builder.complete(); + + for block in fun.body.blocks.values_mut() { + let state = states.remove(&block.id).unwrap(); + block.phis = state.phis; } + Ok(()) } + +fn enter_ssa_impl<'a, 'e, 'f>( + builder: &mut Builder<'a, 'e, 'f>, + instructions: &mut Vec<'a, Instruction<'a>>, +) -> Result<(), SSAError> { + builder.each_block(|block, builder| { + for instr_ix in &block.instructions { + let instr = &mut instructions[usize::from(*instr_ix)]; + instr.each_identifier_store(|store| builder.visit_store(store)); + instr.each_identifier_load(|load| builder.visit_load(load)); + } + }); + Ok(()) +} + +#[derive(Debug)] +struct Builder<'a, 'e, 'f> { + env: &'e Environment<'a>, + blocks: &'f Blocks<'a>, + + states: IndexMap>, + current: BlockId, + unsealed_predecessors: IndexMap, + unknown: IndexSet, + context: IndexSet, +} + +#[derive(Debug)] +struct BlockState<'a> { + defs: IndexMap>, + incomplete_phis: Vec<'a, IncompletePhi<'a>>, + phis: Vec<'a, Phi<'a>>, +} + +impl<'a> BlockState<'a> { + fn new(env: &Environment<'a>) -> Self { + Self { + defs: Default::default(), + incomplete_phis: Vec::new_in(&env.allocator), + phis: Vec::new_in(&env.allocator), + } + } +} + +#[derive(Debug)] +struct IncompletePhi<'a> { + old_id: Identifier<'a>, + new_id: Identifier<'a>, +} + +impl<'a, 'e, 'f> Builder<'a, 'e, 'f> { + fn new(env: &'e Environment<'a>, entry: BlockId, blocks: &'f Blocks<'a>) -> Self { + let states = blocks + .keys() + .map(|block_id| (*block_id, BlockState::new(env))) + .collect(); + Self { + env, + blocks, + states, + current: entry, + unsealed_predecessors: Default::default(), + unknown: Default::default(), + context: Default::default(), + } + } + + fn complete(self) -> IndexMap> { + self.states + } + + fn next_ssa_id(&self) -> IdentifierId { + self.env.next_identifier_id() + } + + fn visit_store(&mut self, lvalue: &mut LValue<'a>) -> () { + let old_identifier = &lvalue.identifier.identifier; + // TODO: use Result (?) + assert!(!self.unknown.contains(&old_identifier.id), "EnterSSA: Expected identifier to be defined before being used. Identifier {old_identifier:?} is undefined."); + + if self.context.contains(&old_identifier.id) { + let new_identifier = self.get_id_at(self.current, old_identifier); + lvalue.identifier.identifier = new_identifier; + return; + } + + let new_identifier = self.make_identifier(old_identifier); + let state = self.states.get_mut(&self.current).unwrap(); + state.defs.insert(old_identifier.id, new_identifier.clone()); + lvalue.identifier.identifier = new_identifier; + } + + fn visit_param(&mut self, param: &mut IdentifierOperand<'a>) -> () { + let old_identifier = ¶m.identifier; + let new_identifier = self.make_identifier(old_identifier); + let state = self.states.get_mut(&self.current).unwrap(); + state.defs.insert(old_identifier.id, new_identifier.clone()); + param.identifier = new_identifier; + } + + fn visit_load(&mut self, local: &mut IdentifierOperand<'a>) -> () { + let new_identifier = self.get_id_at(self.current, &local.identifier); + local.identifier = new_identifier; + } + + fn get_id_at(&mut self, block_id: BlockId, old_identifier: &Identifier<'a>) -> Identifier<'a> { + // Check if we've already resolved this identifier in this block + let state = self.states.get(&block_id).unwrap(); + if let Some(identifier) = state.defs.get(&old_identifier.id) { + return identifier.clone(); + } + // Else we have to look at predecessor blocks: bail if no predecessors + let block = self.blocks.get(&block_id).unwrap(); + if block.predecessors.is_empty() { + println!("Unable to find previous id for {old_identifier:?}"); + self.unknown.insert(old_identifier.id); + return old_identifier.clone(); + } + // If we haven't visited all predecessors, synthesize a new identifier + // and save it as an incomplete phi + if self.unsealed_predecessors.get(&block_id).cloned().unwrap() > 0 { + let new_identifier = self.make_identifier(old_identifier); + let state = self.states.get_mut(&block_id).unwrap(); + state.incomplete_phis.push(IncompletePhi { + old_id: old_identifier.clone(), + new_id: new_identifier.clone(), + }); + state.defs.insert(old_identifier.id, new_identifier.clone()); + return new_identifier; + } + // If exactly one predecessor, check to see if we have a definition there + if block.predecessors.len() == 1 { + let predecessor = block.predecessors.iter().next().unwrap(); + let new_identifier = self.get_id_at(*predecessor, old_identifier); + let state = self.states.get_mut(&block_id).unwrap(); + state.defs.insert(old_identifier.id, new_identifier.clone()); + return new_identifier; + } + // There are multiple predecessors, we may need a phi + let new_identifier = self.make_identifier(old_identifier); + let state = self.states.get_mut(&block_id).unwrap(); + state.defs.insert(old_identifier.id, new_identifier.clone()); + self.add_phi(block_id, old_identifier, new_identifier) + } + + fn add_phi( + &mut self, + block_id: BlockId, + old_identifier: &Identifier<'a>, + new_identifier: Identifier<'a>, + ) -> Identifier<'a> { + let mut phi = Phi { + identifier: new_identifier.clone(), + operands: Default::default(), + }; + let block = self.blocks.get(&block_id).unwrap(); + let preds = block.predecessors.clone(); + for pred_block_id in preds { + let pred_id = self.get_id_at(pred_block_id, old_identifier); + phi.operands.insert(pred_block_id, pred_id); + } + let state = self.states.get_mut(&block_id).unwrap(); + state.phis.push(phi); + new_identifier + } + + fn make_identifier(&self, old_identifier: &Identifier<'a>) -> Identifier<'a> { + let old_data = old_identifier.data.borrow(); + Identifier { + id: self.next_ssa_id(), + name: old_identifier.name.clone(), + data: Rc::new(RefCell::new(IdentifierData { + mutable_range: MutableRange::new(), + scope: None, + type_: old_data.type_.clone(), + })), + } + } + + fn fix_incomplete_phis(&mut self, block_id: BlockId) -> () { + let state = self.states.get_mut(&block_id).unwrap(); + let incomplete_phis = + std::mem::replace(&mut state.incomplete_phis, Vec::new_in(&self.env.allocator)); + for phi in incomplete_phis { + self.add_phi(block_id, &phi.old_id, phi.new_id); + } + } + + fn each_block(&mut self, mut f: F) -> () + where + F: FnMut(&BasicBlock<'a>, &mut Self) -> (), + { + let mut visited = IndexSet::new(); + let block_ids: Vec<_> = self.blocks.keys().cloned().collect_in(self.env.allocator); + for block_id in block_ids { + visited.insert(block_id); + self.current = block_id; + let block = self.blocks.get(&block_id).unwrap(); + f(block, self); + let successors = block.terminal.value.successors(); + for successor in successors { + let block = self.blocks.get(&successor).unwrap(); + let count = self + .unsealed_predecessors + .get(&successor) + .cloned() + .unwrap_or(block.predecessors.len()) + - 1; + self.unsealed_predecessors.insert(successor, count); + if count == 0 && visited.contains(&successor) { + self.fix_incomplete_phis(successor) + } + } + } + } +} diff --git a/compiler/forget/crates/hir/src/basic_block.rs b/compiler/forget/crates/hir/src/basic_block.rs index c6f895ff71..87402c846a 100644 --- a/compiler/forget/crates/hir/src/basic_block.rs +++ b/compiler/forget/crates/hir/src/basic_block.rs @@ -1,6 +1,8 @@ -use std::collections::HashSet; +use std::fmt::Display; -use crate::{id_types::BlockId, InstrIx, Terminal}; +use indexmap::{IndexMap, IndexSet}; + +use crate::{id_types::BlockId, Identifier, InstrIx, Terminal}; /// Represents a sequence of instructions that will always[1] execute /// consecutively. Concretely, a block may have zero or more instructions @@ -26,7 +28,15 @@ pub struct BasicBlock<'a> { pub terminal: Terminal<'a>, /// The immediate predecessors of this block - pub predecessors: HashSet, + pub predecessors: IndexSet, + + pub phis: bumpalo::collections::Vec<'a, Phi<'a>>, +} + +#[derive(Debug)] +pub struct Phi<'a> { + pub identifier: Identifier<'a>, + pub operands: IndexMap>, } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] @@ -36,3 +46,14 @@ pub enum BlockKind { Loop, Sequence, } + +impl Display for BlockKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Block => f.write_str("block"), + Self::Value => f.write_str("value"), + Self::Loop => f.write_str("loop"), + Self::Sequence => f.write_str("sequence"), + } + } +} diff --git a/compiler/forget/crates/hir/src/function.rs b/compiler/forget/crates/hir/src/function.rs index ef8c963924..ac9e832349 100644 --- a/compiler/forget/crates/hir/src/function.rs +++ b/compiler/forget/crates/hir/src/function.rs @@ -1,13 +1,19 @@ use std::cell::Cell; -use bumpalo::{boxed::Box, collections::Vec}; +use bumpalo::{ + boxed::Box, + collections::{String, Vec}, +}; use indexmap::IndexMap; -use crate::{BasicBlock, BlockId, Instruction}; +use crate::{BasicBlock, BlockId, IdentifierOperand, Instruction}; /// Represents either a React function or a function expression +#[derive(Debug)] pub struct Function<'a> { + pub id: Option>, pub body: HIR<'a>, + pub params: Vec<'a, IdentifierOperand<'a>>, pub is_async: bool, pub is_generator: bool, } @@ -16,18 +22,21 @@ pub struct Function<'a> { /// Blocks are stored in reverse postorder (predecessors before successors) /// so that compiler passes can complete forward data flow analysis in a /// single pass over the CFG in the case where there are no loops. +#[derive(Debug)] pub struct HIR<'a> { /// The id of the first block pub entry: BlockId, /// Blocks are stored in a map for easy retrieval by their id, /// but the blocks are in reverse postorder - pub blocks: IndexMap>>, + pub blocks: Blocks<'a>, /// All instructions for the block. This may contain unused items, pub instructions: Vec<'a, Instruction<'a>>, } +pub type Blocks<'a> = IndexMap>>; + impl<'a> HIR<'a> { pub fn block(&self, id: BlockId) -> &BasicBlock<'a> { self.blocks.get(&id).unwrap() diff --git a/compiler/forget/crates/hir/src/instruction.rs b/compiler/forget/crates/hir/src/instruction.rs index 66f62a3119..b251f04368 100644 --- a/compiler/forget/crates/hir/src/instruction.rs +++ b/compiler/forget/crates/hir/src/instruction.rs @@ -11,6 +11,48 @@ pub struct Instruction<'a> { pub value: InstructionValue<'a>, } +impl<'a> Instruction<'a> { + pub fn each_identifier_store(&mut self, mut f: F) -> () + where + F: FnMut(&mut LValue<'a>) -> (), + { + match &mut self.value { + InstructionValue::Array(_) => {} + InstructionValue::Binary(_) => {} + InstructionValue::DeclareContext(instr) => { + f(&mut instr.lvalue); + } + InstructionValue::DeclareLocal(instr) => { + f(&mut instr.lvalue); + } + InstructionValue::LoadContext(_) => {} + InstructionValue::LoadGlobal(_) => {} + InstructionValue::LoadLocal(_) => {} + InstructionValue::Primitive(_) => {} + InstructionValue::StoreLocal(instr) => { + f(&mut instr.lvalue); + } + } + } + + pub fn each_identifier_load(&mut self, mut f: F) -> () + where + F: FnMut(&mut IdentifierOperand<'a>) -> (), + { + match &mut self.value { + InstructionValue::Array(_) => {} + InstructionValue::Binary(_) => {} + InstructionValue::DeclareContext(_) => {} + InstructionValue::DeclareLocal(_) => {} + InstructionValue::LoadContext(_) => {} + InstructionValue::LoadGlobal(_) => {} + InstructionValue::LoadLocal(instr) => f(&mut instr.place), + InstructionValue::Primitive(_) => {} + InstructionValue::StoreLocal(_) => {} + } + } +} + #[derive(Debug)] pub enum InstructionValue<'a> { Array(Array<'a>), diff --git a/compiler/forget/crates/hir/src/print.rs b/compiler/forget/crates/hir/src/print.rs index 0e6619bc38..d9e0ef4c01 100644 --- a/compiler/forget/crates/hir/src/print.rs +++ b/compiler/forget/crates/hir/src/print.rs @@ -2,7 +2,7 @@ use std::fmt::{Result, Write}; use crate::{ ArrayElement, BasicBlock, Function, Identifier, IdentifierOperand, Instruction, - InstructionValue, LValue, Operand, PrimitiveValue, Terminal, TerminalValue, HIR, + InstructionValue, LValue, Operand, Phi, PrimitiveValue, Terminal, TerminalValue, HIR, }; /// Trait for HIR types to describe how they print themselves. @@ -16,6 +16,20 @@ pub trait Print<'a> { impl<'a> Print<'a> for Function<'a> { fn print(&self, hir: &HIR<'a>, out: &mut impl Write) -> Result { + writeln!( + out, + "function {}(", + match &self.id { + Some(id) => id, + None => "", + } + )?; + for param in &self.params { + write!(out, " ")?; + param.print(hir, out)?; + writeln!(out, ",")?; + } + writeln!(out, ")")?; writeln!(out, "entry {}", self.body.entry)?; for (_, block) in self.body.blocks.iter() { block.print(hir, out)?; @@ -26,7 +40,21 @@ impl<'a> Print<'a> for Function<'a> { impl<'a> Print<'a> for BasicBlock<'a> { fn print(&self, hir: &HIR<'a>, out: &mut impl Write) -> Result { - writeln!(out, "{}", self.id)?; + writeln!(out, "{} ({})", self.id, self.kind)?; + if !self.predecessors.is_empty() { + write!(out, " predecessors: ")?; + for (ix, pred) in self.predecessors.iter().enumerate() { + if ix != 0 { + write!(out, ", ")?; + } + write!(out, "{}", *pred)?; + } + writeln!(out)?; + } + for phi in self.phis.iter() { + phi.print(hir, out)?; + writeln!(out)?; + } for ix in &self.instructions { let instr = &hir.instructions[usize::from(*ix)]; write!(out, " {} {} = ", instr.id, ix)?; @@ -38,6 +66,23 @@ impl<'a> Print<'a> for BasicBlock<'a> { } } +impl<'a> Print<'a> for Phi<'a> { + fn print(&self, hir: &HIR<'a>, out: &mut impl Write) -> Result { + write!(out, " ")?; + self.identifier.print(hir, out)?; + write!(out, ": phi(")?; + for (ix, (pred_id, id)) in self.operands.iter().enumerate() { + if ix != 0 { + write!(out, ", ")?; + } + write!(out, "{}: ", pred_id)?; + id.print(hir, out)?; + } + write!(out, ")")?; + Ok(()) + } +} + impl<'a> Print<'a> for Instruction<'a> { fn print(&self, hir: &HIR<'a>, out: &mut impl Write) -> Result { write!(out, " {} ", self.id)?; diff --git a/compiler/forget/crates/hir/src/types.rs b/compiler/forget/crates/hir/src/types.rs index 324bf6318e..d7e5ccd5cd 100644 --- a/compiler/forget/crates/hir/src/types.rs +++ b/compiler/forget/crates/hir/src/types.rs @@ -1,6 +1,6 @@ use crate::{FunctionId, ObjectId, TypeVarId}; -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum Type { Builtin(BuiltinType), // Phi(Box), @@ -9,7 +9,7 @@ pub enum Type { // Prop(Box), } -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum BuiltinType { Primitive, Function(Option),