@samitouri / QOS-React-2 / commits / 2ac69bf7d9

[rust] Passes from HIRBuilder

This is a start to porting HIRBuilder, with a largely complete implementation of `build()`. Notably this includes all the passes which build() calls, and the helper functions those passes call in turn: ```rust 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); ``` This was pretty straightforward. I ran into one borrow checker issue with (iirc) `remove_unreachable_for_updates` where i needed to simultaneously hold a mutable reference into the HIR (to write the ForTerminal) and also get an immutable reference to check if the update block is reachable. The challenge is this: ```rust for block in hir.blocks.values_mut() { ^^^^^^^^^^^^^^^^^^^^^^^ hir borrowed mutably here if let TerminalValue::ForTerminal(terminal) = &mut block.terminal.value { if let Some(update) = terminal.update { if !hir.blocks.contains(&update) { ^^^^^^^^^^ borrowed immutably here terminal.update = None; ^^^^^^^^ mutable borrow still active here (and also bc of the loop) } } } } ``` I quickly worked around this as we do in the other passes here by first building a set of the block ids contained in the function (so that the `hir.blocks.contains()` call becomes a call to the copied set of block ids). An alternative would be to add a helper function for mutable iteration which takes the desired value out of the data structure so that you can safely mutate it and reference the rest of the HIR. Usage might look like this: ```rust hir.blocks.each_mut(|mut block, hir| { if let TerminalValue::ForTerminal(terminal) = &mut block.terminal.value { if let Some(update) = terminal.update { if !hir.blocks.contains(&update) { terminal.update = None; } } } }) ``` The lambda would receive the current `block` as a mutable reference, and for the duration of the call `hir.blocks[block.id]` would be set to a sentinel value that would crash if accessed. Meanwhile, `hir` would be a readonly reference to the HIR, allowing the lambda to otherwise lookup information on the HIR but not mutate it. This seems...kinda fine? But also not immediately necessary as there's an easy and efficient-enough-workaround for the cases i've encountered so far.

Joe Savona committed Jul 6, 2023 at 09:24 UTC 2ac69bf7d952aba132cd887b09b71622a295b88f
11 files changed +345 -25
compiler/forget/Cargo.lock
+2
@@ -113,7 +113,9 @@ dependencies = [
113 name = "build-hir"
114 version = "0.1.0"
115 dependencies = [
116 + "estree",
117 "hir",
118 + "indexmap 2.0.0",
119 ]
120
121 [[package]]
compiler/forget/crates/build-hir/Cargo.toml
+3 -1
@@ -6,4 +6,6 @@ edition = "2021"
6 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7
8 [dependencies]
9 -hir = { path = "../hir" }
\ No newline at end of file
9 +hir = { path = "../hir" }
10 +estree = { path = "../estree" }
11 +indexmap = "2.0.0"
compiler/forget/crates/build-hir/src/build.rs
+28
@@ -0,0 +1,28 @@
1 +use estree::FunctionDeclaration;
2 +use hir::{Environment, Function};
3 +
4 +use crate::builder::Builder;
5 +
6 +/// Converts a React function in ESTree format into HIR. Returns the HIR
7 +/// if it was constructed sucessfully, otherwise a list of diagnostics
8 +/// if the input could be not be converted to HIR.
9 +///
10 +/// Failures generally include nonsensical input (`delete 1`) or syntax
11 +/// that is not yet supported.
12 +pub fn build<'a>(
13 + environment: &'a Environment<'a>,
14 + fun: FunctionDeclaration,
15 +) -> Result<Function<'a>, Vec<()>> {
16 + let mut builder = Builder::new(environment);
17 +
18 + let body = match builder.build() {
19 + Ok(body) => body,
20 + Err(diagnostic) => return Err(vec![diagnostic]),
21 + };
22 +
23 + Ok(Function {
24 + body,
25 + is_async: fun.is_async,
26 + is_generator: fun.is_generator,
27 + })
28 +}
compiler/forget/crates/build-hir/src/builder.rs
+187 -3
@@ -1,6 +1,7 @@
1 -use std::collections::HashMap;
1 +use std::collections::HashSet;
2
3 -use hir::{BasicBlock, BlockId, Environment};
3 +use hir::{BasicBlock, BlockId, Environment, GotoKind, InstructionIdGenerator, TerminalValue, HIR};
4 +use indexmap::IndexMap;
5
6 /// Helper struct used when converting from ESTree to HIR. Includes:
7 /// - Variable resolution
@@ -12,7 +13,190 @@ use hir::{BasicBlock, BlockId, Environment};
13 /// labels and variables, and then calling `build()` when the HIR
14 /// is complete.
15 pub struct Builder<'a> {
16 + #[allow(dead_code)]
17 environment: &'a Environment<'a>,
16 - completed: HashMap<BlockId, BasicBlock<'a>>,
18 +
19 + completed: IndexMap<BlockId, BasicBlock<'a>>,
20 +
21 entry: BlockId,
22 }
23 +
24 +impl<'a> Builder<'a> {
25 + pub(crate) fn new(environment: &'a Environment<'a>) -> Self {
26 + let entry = environment.next_block_id();
27 + Self {
28 + environment,
29 + completed: Default::default(),
30 + entry,
31 + }
32 + }
33 +
34 + /// Completes the builder and returns the HIR if it was valid,
35 + /// or a Diagnostic if a validation error occured.
36 + ///
37 + /// TODO: refine the type, only invariants should be possible here,
38 + /// not other types of errors
39 + pub(crate) fn build(self) -> Result<HIR<'a>, Diagnostic> {
40 + let mut hir = HIR {
41 + entry: self.entry,
42 + blocks: self.completed,
43 + };
44 +
45 + reverse_postorder_blocks(&mut hir);
46 + remove_unreachable_for_updates(&mut hir);
47 + remove_unreachable_fallthroughs(&mut hir);
48 + remove_unreachable_do_while_statements(&mut hir);
49 + mark_instruction_ids(&mut hir)?;
50 + mark_predecessors(&mut hir);
51 +
52 + Ok(hir)
53 + }
54 +}
55 +
56 +/// Modifies the HIR to put the blocks in reverse postorder, with predecessors before
57 +/// successors (except for the case of loops)
58 +fn reverse_postorder_blocks<'a>(hir: &mut HIR<'a>) {
59 + let mut visited = HashSet::<BlockId>::with_capacity(hir.blocks.len());
60 + let mut postorder = Vec::<BlockId>::with_capacity(hir.blocks.len());
61 + fn visit<'a>(
62 + block_id: BlockId,
63 + hir: &HIR<'a>,
64 + visited: &mut HashSet<BlockId>,
65 + postorder: &mut Vec<BlockId>,
66 + ) {
67 + if !visited.insert(block_id) {
68 + // already visited
69 + return;
70 + }
71 + let block = hir.block(block_id);
72 + let terminal = &block.terminal;
73 + match &terminal.value {
74 + TerminalValue::IfTerminal(terminal) => {
75 + visit(terminal.alternate, hir, visited, postorder);
76 + visit(terminal.consequent, hir, visited, postorder);
77 + }
78 + TerminalValue::ForTerminal(terminal) => {
79 + visit(terminal.init, hir, visited, postorder);
80 + }
81 + TerminalValue::DoWhileTerminal(terminal) => {
82 + visit(terminal.body, hir, visited, postorder);
83 + }
84 + TerminalValue::GotoTerminal(terminal) => {
85 + visit(terminal.block, hir, visited, postorder);
86 + }
87 + TerminalValue::ReturnTerminal(..) => { /* no-op */ }
88 + }
89 + }
90 + visit(hir.entry, &hir, &mut visited, &mut postorder);
91 +
92 + // NOTE: could consider sorting the blocks in-place by key
93 + let mut blocks = IndexMap::with_capacity(hir.blocks.len());
94 + for id in postorder.iter().rev().cloned() {
95 + blocks.insert(id, hir.blocks.remove(&id).unwrap());
96 + }
97 +
98 + hir.blocks = blocks;
99 +}
100 +
101 +/// Prunes ForTerminal.update values (sets to None) if they are unreachable
102 +fn remove_unreachable_for_updates<'a>(hir: &mut HIR<'a>) {
103 + let block_ids: HashSet<BlockId> = hir.blocks.keys().cloned().collect();
104 +
105 + for block in hir.blocks.values_mut() {
106 + if let TerminalValue::ForTerminal(terminal) = &mut block.terminal.value {
107 + if let Some(update) = terminal.update {
108 + if !block_ids.contains(&update) {
109 + terminal.update = None;
110 + }
111 + }
112 + }
113 + }
114 +}
115 +
116 +/// Prunes unreachable fallthrough values, setting them to None if the referenced
117 +/// block was not otherwise reachable.
118 +fn remove_unreachable_fallthroughs<'a>(hir: &mut HIR<'a>) {
119 + let block_ids: HashSet<BlockId> = hir.blocks.keys().cloned().collect();
120 +
121 + for block in hir.blocks.values_mut() {
122 + block
123 + .terminal
124 + .value
125 + .map_optional_fallthroughs(|fallthrough| {
126 + if block_ids.contains(&fallthrough) {
127 + Some(fallthrough)
128 + } else {
129 + None
130 + }
131 + })
132 + }
133 +}
134 +
135 +/// Rewrites DoWhile statements into Gotos if the test block is not reachable
136 +fn remove_unreachable_do_while_statements<'a>(hir: &mut HIR<'a>) {
137 + let block_ids: HashSet<BlockId> = hir.blocks.keys().cloned().collect();
138 +
139 + for block in hir.blocks.values_mut() {
140 + if let TerminalValue::DoWhileTerminal(terminal) = &mut block.terminal.value {
141 + if !block_ids.contains(&terminal.test) {
142 + block.terminal.value = TerminalValue::GotoTerminal(hir::GotoTerminal {
143 + block: terminal.body,
144 + kind: GotoKind::Break,
145 + });
146 + }
147 + }
148 + }
149 +}
150 +
151 +/// Updates the instruction ids for all instructions and blocks
152 +/// Relies on the blocks being in reverse postorder to ensure that id ordering is correct
153 +fn mark_instruction_ids<'a>(hir: &mut HIR<'a>) -> Result<(), Diagnostic> {
154 + let mut id_gen = InstructionIdGenerator::new();
155 + let mut visited = HashSet::<(usize, usize)>::new();
156 + for (block_ix, block) in hir.blocks.values_mut().enumerate() {
157 + for (instr_ix, instr) in block.instructions.iter_mut().enumerate() {
158 + invariant(!visited.insert((block_ix, instr_ix)), || ())?;
159 + instr.id = id_gen.next();
160 + }
161 + block.terminal.id = id_gen.next();
162 + }
163 + Ok(())
164 +}
165 +
166 +/// Updates the predecessors of each block
167 +fn mark_predecessors<'a>(hir: &mut HIR<'a>) {
168 + for block in hir.blocks.values_mut() {
169 + block.predecessors.clear();
170 + }
171 + let mut visited = HashSet::<BlockId>::with_capacity(hir.blocks.len());
172 + fn visit<'a>(
173 + block_id: BlockId,
174 + prev_id: Option<BlockId>,
175 + hir: &mut HIR<'a>,
176 + visited: &mut HashSet<BlockId>,
177 + ) {
178 + let block = hir.block_mut(block_id);
179 + if let Some(prev_id) = prev_id {
180 + block.predecessors.insert(prev_id);
181 + }
182 + if !visited.insert(block_id) {
183 + return;
184 + }
185 + for successor in block.terminal.value.successors() {
186 + visit(successor, Some(block_id), hir, visited)
187 + }
188 + }
189 + visit(hir.entry, None, hir, &mut visited);
190 +}
191 +
192 +fn invariant<F>(cond: bool, _f: F) -> Result<(), Diagnostic>
193 +where
194 + F: FnOnce() -> Diagnostic,
195 +{
196 + if !cond {
197 + panic!("Oops invariant failed");
198 + }
199 + Ok(())
200 +}
201 +
202 +type Diagnostic = ();
compiler/forget/crates/build-hir/src/lib.rs
+2
@@ -1,2 +1,4 @@
1 mod build;
2 mod builder;
3 +
4 +pub use build::build;
compiler/forget/crates/hir/src/basic_block.rs
+9 -1
@@ -1,4 +1,6 @@
1 -use crate::{id_types::BlockId, Instruction};
1 +use std::collections::HashSet;
2 +
3 +use crate::{id_types::BlockId, Instruction, Terminal};
4
5 /// Represents a sequence of instructions that will always[1] execute
6 /// consecutively. Concretely, a block may have zero or more instructions
@@ -18,6 +20,12 @@ pub struct BasicBlock<'a> {
20
21 /// The ordered instructions in this block
22 pub instructions: bumpalo::collections::Vec<'a, Instruction<'a>>,
23 +
24 + /// The terminal instruction for the block
25 + pub terminal: Terminal<'a>,
26 +
27 + /// The immediate predecessors of this block
28 + pub predecessors: HashSet<BlockId>,
29 }
30
31 pub enum BlockKind {
compiler/forget/crates/hir/src/environment.rs
+12 -10
@@ -1,3 +1,5 @@
1 +use std::cell::Cell;
2 +
3 use bumpalo::Bump;
4
5 use crate::{BlockId, Features, IdentifierId, Registry};
@@ -20,10 +22,10 @@ pub struct Environment<'a> {
22 allocator: &'a Bump,
23
24 /// The next available block index
23 - next_block_id: BlockId,
25 + next_block_id: Cell<BlockId>,
26
27 /// The next available identifier id
26 - next_identifier_id: IdentifierId,
28 + next_identifier_id: Cell<IdentifierId>,
29 }
30
31 impl<'a> Environment<'a> {
@@ -32,8 +34,8 @@ impl<'a> Environment<'a> {
34 allocator,
35 features,
36 registry,
35 - next_block_id: BlockId(0),
36 - next_identifier_id: IdentifierId(0),
37 + next_block_id: Cell::new(BlockId(0)),
38 + next_identifier_id: Cell::new(IdentifierId(0)),
39 }
40 }
41
@@ -43,16 +45,16 @@ impl<'a> Environment<'a> {
45 }
46
47 /// Get the next available block id
46 - pub fn next_block_id(&mut self) -> BlockId {
47 - let id = self.next_block_id;
48 - self.next_block_id = id.next();
48 + pub fn next_block_id(&self) -> BlockId {
49 + let id = self.next_block_id.get();
50 + self.next_block_id.set(id.next());
51 id
52 }
53
54 /// Get the next available identifier id
53 - pub fn next_identifier_id(&mut self) -> IdentifierId {
54 - let id = self.next_identifier_id;
55 - self.next_identifier_id = id.next();
55 + pub fn next_identifier_id(&self) -> IdentifierId {
56 + let id = self.next_identifier_id.get();
57 + self.next_identifier_id.set(id.next());
58 id
59 }
60 }
compiler/forget/crates/hir/src/function.rs
+10
@@ -21,3 +21,13 @@ pub struct HIR<'a> {
21 /// but the blocks are in reverse postorder
22 pub blocks: IndexMap<BlockId, BasicBlock<'a>>,
23 }
24 +
25 +impl<'a> HIR<'a> {
26 + pub fn block(&self, id: BlockId) -> &BasicBlock<'a> {
27 + self.blocks.get(&id).unwrap()
28 + }
29 +
30 + pub fn block_mut(&mut self, id: BlockId) -> &mut BasicBlock<'a> {
31 + self.blocks.get_mut(&id).unwrap()
32 + }
33 +}
compiler/forget/crates/hir/src/id_types.rs
+14
@@ -32,6 +32,20 @@ impl IdentifierId {
32 /// and to reflect that it is applied to terminals as well
33 pub struct InstructionId(pub(crate) u32);
34
35 +pub struct InstructionIdGenerator(u32);
36 +
37 +impl InstructionIdGenerator {
38 + pub fn new() -> Self {
39 + Self(0)
40 + }
41 +
42 + pub fn next(&mut self) -> InstructionId {
43 + let id = self.0;
44 + self.0 += 1;
45 + InstructionId(id)
46 + }
47 +}
48 +
49 /// Uniquely identifies a reactive scope
50 pub struct ScopeId(pub(crate) u32);
51
compiler/forget/crates/hir/src/lib.rs
+5 -5
@@ -8,12 +8,12 @@ mod registry;
8 mod terminal;
9 mod types;
10
11 -pub use basic_block::BasicBlock;
12 -pub use environment::Environment;
13 -pub use features::Features;
14 -pub use function::Function;
11 +pub use basic_block::*;
12 +pub use environment::*;
13 +pub use features::*;
14 +pub use function::*;
15 pub use id_types::*;
16 -pub use instruction::Instruction;
16 +pub use instruction::*;
17 pub use registry::Registry;
18 pub use terminal::*;
19 pub use types::*;
compiler/forget/crates/hir/src/terminal.rs
+73 -5
@@ -1,3 +1,5 @@
1 +use std::iter::Successors;
2 +
3 use crate::{instruction::Place, BlockId, InstructionId};
4
5 /// Terminals represent statements or expressions that affect control flow,
@@ -9,10 +11,10 @@ pub struct Terminal<'a> {
11
12 pub enum TerminalValue<'a> {
13 // BranchTerminal(BranchTerminal),
12 - // DoWhileTerminal(DoWhileTerminal),
14 + DoWhileTerminal(DoWhileTerminal),
15 // ForOfTerminal(ForOfTerminal),
14 - // ForTerminal(ForTerminal),
15 - // GotoTerminal(GotoTerminal),
16 + ForTerminal(ForTerminal),
17 + GotoTerminal(GotoTerminal),
18 IfTerminal(IfTerminal<'a>),
19 // LabelTerminal(LabelTerminal),
20 // LogicalTerminal(LogicalTerminal),
@@ -26,15 +28,81 @@ pub enum TerminalValue<'a> {
28 // WhileTerminal(WhileTerminal),
29 }
30
31 +impl<'a> TerminalValue<'a> {
32 + pub fn map_optional_fallthroughs<F>(&mut self, f: F) -> ()
33 + where
34 + F: Fn(BlockId) -> Option<BlockId>,
35 + {
36 + match self {
37 + Self::IfTerminal(terminal) => {
38 + terminal.fallthrough = match terminal.fallthrough {
39 + Some(fallthrough) => f(fallthrough),
40 + _ => None,
41 + }
42 + }
43 + Self::DoWhileTerminal(DoWhileTerminal { fallthrough, .. })
44 + | Self::ForTerminal(ForTerminal { fallthrough, .. }) => {
45 + // statically detect if fallthrough is changed to Option so
46 + // that we can update to map the fallthrough w f()
47 + let _: BlockId = *fallthrough;
48 + }
49 + Self::GotoTerminal(_) | Self::ReturnTerminal(_) => {}
50 + }
51 + }
52 +
53 + pub fn successors(&self) -> Vec<BlockId> {
54 + match self {
55 + Self::IfTerminal(terminal) => {
56 + vec![terminal.consequent, terminal.alternate]
57 + }
58 + Self::ForTerminal(terminal) => {
59 + vec![terminal.init]
60 + }
61 + Self::DoWhileTerminal(terminal) => {
62 + vec![terminal.body]
63 + }
64 + Self::GotoTerminal(terminal) => {
65 + vec![terminal.block]
66 + }
67 + Self::ReturnTerminal(_) => {
68 + vec![]
69 + }
70 + }
71 + }
72 +}
73 +
74 +pub struct GotoTerminal {
75 + pub block: BlockId,
76 + pub kind: GotoKind,
77 +}
78 +
79 +#[derive(Clone, Copy)]
80 +pub enum GotoKind {
81 + Break,
82 + Continue,
83 +}
84 +
85 +pub struct DoWhileTerminal {
86 + pub body: BlockId,
87 + pub test: BlockId,
88 + pub fallthrough: BlockId,
89 +}
90 +
91 pub struct IfTerminal<'a> {
92 pub test: Place<'a>,
93 pub consequent: BlockId,
94 pub alternate: BlockId,
95 pub fallthrough: Option<BlockId>,
34 - pub id: InstructionId,
96 }
97
98 pub struct ReturnTerminal<'a> {
99 pub value: Place<'a>,
39 - pub id: InstructionId,
100 +}
101 +
102 +pub struct ForTerminal {
103 + pub init: BlockId,
104 + pub test: BlockId,
105 + pub update: Option<BlockId>,
106 + pub body: BlockId,
107 + pub fallthrough: BlockId,
108 }