[rust] Start of lowering (BuildHIR)
Starts to port BuildHIR, in the Rust case this means the ESTree -> HIR conversion. This necessitated flushing out the Builder struct a bit more. Mostly the logic translates over very directly, and if anything it's cleaner because of the lack of noise dealing with TypeScript unsoundness for Babel typedefs and switch statements.
Joe Savona committed
Jul 6, 2023 at 09:24 UTC
b1c8cb358b913564c83c64bb579e460a1e0bef43
6 files changed
+305
-12
compiler/forget/Cargo.lock
+1
@@ -113,6 +113,7 @@ dependencies = [
113
name = "build-hir"
114
version = "0.1.0"
115
dependencies = [
116
+ "bumpalo",
117
"estree",
118
"hir",
119
"indexmap 2.0.0",
compiler/forget/crates/build-hir/Cargo.toml
+1
@@ -9,3 +9,4 @@ edition = "2021"
9
hir = { path = "../hir" }
10
estree = { path = "../estree" }
11
indexmap = "2.0.0"
12
+bumpalo = "3.13.0"
compiler/forget/crates/build-hir/src/build.rs
+185
-7
@@ -1,5 +1,9 @@
1
-use estree::FunctionDeclaration;
2
-use hir::{Environment, Function};
1
+use bumpalo::collections::{CollectIn, String};
2
+use estree::{ExpressionLike, FunctionDeclaration, Literal, LiteralValue, Statement};
3
+use hir::{
4
+ ArrayElement, BlockKind, Environment, Function, GotoKind, Identifier, InstructionValue,
5
+ LoadLocal, Place, PrimitiveValue, TerminalValue,
6
+};
7
8
use crate::builder::Builder;
9
@@ -12,17 +16,191 @@ use crate::builder::Builder;
16
pub fn build<'a>(
17
environment: &'a Environment<'a>,
18
fun: FunctionDeclaration,
15
-) -> Result<Function<'a>, Vec<()>> {
19
+) -> Result<Function<'a>, Diagnostic> {
20
let mut builder = Builder::new(environment);
21
18
- let body = match builder.build() {
19
- Ok(body) => body,
20
- Err(diagnostic) => return Err(vec![diagnostic]),
21
- };
22
+ lower_statement(environment, &mut builder, fun.body.unwrap(), None)?;
23
24
+ // In case the function did not explicitly return, terminate the final
25
+ // block with an explicit `return undefined`. If the function *did* return,
26
+ // this will be unreachable and get pruned later.
27
+ let implicit_return_value = lower_value_to_temporary(
28
+ environment,
29
+ &mut builder,
30
+ InstructionValue::Primitive(hir::Primitive {
31
+ value: PrimitiveValue::Undefined,
32
+ }),
33
+ );
34
+ builder.terminate(
35
+ TerminalValue::ReturnTerminal(hir::ReturnTerminal {
36
+ value: implicit_return_value,
37
+ }),
38
+ hir::BlockKind::Block,
39
+ );
40
+
41
+ let body = builder.build()?;
42
Ok(Function {
43
body,
44
is_async: fun.is_async,
45
is_generator: fun.is_generator,
46
})
47
}
48
+
49
+/// Convert a statement to HIR. This will often result in multiple instructions and blocks
50
+/// being created as statements often describe control flow.
51
+fn lower_statement<'a>(
52
+ env: &'a Environment<'a>,
53
+ builder: &mut Builder<'a>,
54
+ stmt: Statement,
55
+ label: Option<String<'a>>,
56
+) -> Result<(), Diagnostic> {
57
+ match stmt {
58
+ Statement::BlockStatement(stmt) => {
59
+ for stmt in stmt.body {
60
+ lower_statement(env, builder, stmt, None)?;
61
+ }
62
+ }
63
+ Statement::BreakStatement(stmt) => {
64
+ let block = builder.resolve_break(stmt.label)?;
65
+ builder.terminate(
66
+ TerminalValue::GotoTerminal(hir::GotoTerminal {
67
+ block,
68
+ kind: GotoKind::Break,
69
+ }),
70
+ BlockKind::Block,
71
+ );
72
+ }
73
+ Statement::ContinueStatement(stmt) => {
74
+ let block = builder.resolve_continue(stmt.label)?;
75
+ builder.terminate(
76
+ TerminalValue::GotoTerminal(hir::GotoTerminal {
77
+ block,
78
+ kind: GotoKind::Continue,
79
+ }),
80
+ BlockKind::Block,
81
+ );
82
+ }
83
+ Statement::ReturnStatement(stmt) => {
84
+ let value = match stmt.argument {
85
+ Some(argument) => lower_expression_to_temporary(env, builder, argument),
86
+ None => lower_value_to_temporary(
87
+ env,
88
+ builder,
89
+ InstructionValue::Primitive(hir::Primitive {
90
+ value: PrimitiveValue::Undefined,
91
+ }),
92
+ ),
93
+ };
94
+ builder.terminate(
95
+ TerminalValue::ReturnTerminal(hir::ReturnTerminal { value }),
96
+ BlockKind::Block,
97
+ );
98
+ }
99
+ Statement::ExpressionStatement(stmt) => {
100
+ // TODO: port the logic for emitting an ExpressionStatement instr if the instr
101
+ // was a logical or conditional. is that even necessary anymore?
102
+ lower_expression_to_temporary(env, builder, stmt.expression);
103
+ }
104
+ Statement::EmptyStatement(_) => {
105
+ // no-op
106
+ }
107
+ _ => todo!(),
108
+ }
109
+ Ok(())
110
+}
111
+
112
+/// Shortcut for lowering an expression and saving the result to a temporary
113
+fn lower_expression_to_temporary<'a>(
114
+ env: &'a Environment<'a>,
115
+ builder: &mut Builder<'a>,
116
+ expr: ExpressionLike,
117
+) -> Place<'a> {
118
+ let value = lower_expression(env, builder, expr);
119
+ lower_value_to_temporary(env, builder, value)
120
+}
121
+
122
+/// Converts an ESTree Expression into an HIR InstructionValue. Note that while only a single
123
+/// InstructionValue is returned, this function is recursive and may cause multiple instructions
124
+/// to be emitted, possibly across multiple basic blocks (in the case of expressions with control
125
+/// flow semenatics such as logical, conditional, and optional expressions).
126
+fn lower_expression<'a>(
127
+ env: &'a Environment<'a>,
128
+ builder: &mut Builder<'a>,
129
+ expr: ExpressionLike,
130
+) -> InstructionValue<'a> {
131
+ match expr {
132
+ ExpressionLike::Literal(expr) => InstructionValue::Primitive(hir::Primitive {
133
+ value: lower_primitive(env, builder, *expr),
134
+ }),
135
+ ExpressionLike::ArrayExpression(expr) => {
136
+ let elements = expr
137
+ .elements
138
+ .into_iter()
139
+ .map(|expr| match expr {
140
+ ExpressionLike::SpreadElement(expr) => ArrayElement::Spread(
141
+ lower_expression_to_temporary(env, builder, expr.argument),
142
+ ),
143
+ _ => ArrayElement::Place(lower_expression_to_temporary(env, builder, expr)),
144
+ })
145
+ .collect_in(env.allocator);
146
+ InstructionValue::Array(hir::Array { elements })
147
+ }
148
+ // Cases that cannot appear in expression position but which are included in ExpressionLike
149
+ // to make serialization easier
150
+ ExpressionLike::SpreadElement(_) => {
151
+ panic!("SpreadElement may not appear in normal expression position")
152
+ }
153
+ _ => todo!(),
154
+ }
155
+}
156
+
157
+/// Given an already lowered InstructionValue:
158
+/// - if the instruction is a LoadLocal for a temporary location, avoid the indirection
159
+/// and return the place that the LoadLocal loads from
160
+/// - otherwise, create a new temporary place, push an instruction to associate the value with
161
+/// that temporary, and return a clone of the temporary
162
+fn lower_value_to_temporary<'a>(
163
+ env: &'a Environment<'a>,
164
+ builder: &mut Builder<'a>,
165
+ value: InstructionValue<'a>,
166
+) -> Place<'a> {
167
+ if let InstructionValue::LoadLocal(LoadLocal {
168
+ place:
169
+ place @ Place {
170
+ identifier: Identifier { name: None, .. },
171
+ ..
172
+ },
173
+ }) = value
174
+ {
175
+ return place;
176
+ }
177
+ let place = build_temporary_place(env, builder);
178
+ builder.push(todo!("clone `place`"), value);
179
+ return place;
180
+}
181
+
182
+/// Constructs a temporary Identifier and Place wrapper, which can be used as an Instruction lvalue
183
+/// or other places where a temporary target is required
184
+fn build_temporary_place<'a>(env: &'a Environment<'a>, builder: &mut Builder<'a>) -> Place<'a> {
185
+ Place {
186
+ identifier: builder.make_temporary(),
187
+ effect: None,
188
+ }
189
+}
190
+
191
+/// Converts an ESTree literal into a HIR primitive
192
+fn lower_primitive<'a>(
193
+ env: &'a Environment<'a>,
194
+ _builder: &mut Builder<'a>,
195
+ literal: Literal,
196
+) -> PrimitiveValue<'a> {
197
+ match literal.value {
198
+ LiteralValue::Boolean(bool) => PrimitiveValue::Boolean(bool),
199
+ LiteralValue::Null => PrimitiveValue::Null,
200
+ LiteralValue::Number(value) => PrimitiveValue::Number(f64::from(value).into()),
201
+ LiteralValue::String(s) => PrimitiveValue::String(String::from_str_in(&s, &env.allocator)),
202
+ _ => todo!(),
203
+ }
204
+}
205
+
206
+type Diagnostic = ();
compiler/forget/crates/build-hir/src/builder.rs
+87
-3
@@ -1,6 +1,11 @@
1
+use bumpalo::collections::Vec;
2
+use estree::Identifier;
3
use std::collections::HashSet;
4
3
-use hir::{BasicBlock, BlockId, Environment, GotoKind, InstructionIdGenerator, TerminalValue, HIR};
5
+use hir::{
6
+ BasicBlock, BlockId, BlockKind, Environment, GotoKind, Instruction, InstructionIdGenerator,
7
+ InstructionValue, Place, Terminal, TerminalValue, Type, HIR,
8
+};
9
use indexmap::IndexMap;
10
11
/// Helper struct used when converting from ESTree to HIR. Includes:
@@ -19,15 +24,26 @@ pub struct Builder<'a> {
24
completed: IndexMap<BlockId, BasicBlock<'a>>,
25
26
entry: BlockId,
27
+
28
+ wip: WipBlock<'a>,
29
+
30
+ id_gen: InstructionIdGenerator,
31
}
32
33
impl<'a> Builder<'a> {
34
pub(crate) fn new(environment: &'a Environment<'a>) -> Self {
35
let entry = environment.next_block_id();
36
+ let current = WipBlock {
37
+ id: entry,
38
+ kind: BlockKind::Block,
39
+ instructions: Vec::new_in(&environment.allocator),
40
+ };
41
Self {
42
environment,
43
completed: Default::default(),
44
entry,
45
+ wip: current,
46
+ id_gen: InstructionIdGenerator::new(),
47
}
48
}
49
@@ -51,18 +67,80 @@ impl<'a> Builder<'a> {
67
68
Ok(hir)
69
}
70
+
71
+ /// Adds a new instruction to the end of the work in progress block
72
+ pub(crate) fn push(&mut self, lvalue: Place<'a>, value: InstructionValue<'a>) {
73
+ let instr = Instruction {
74
+ id: self.id_gen.next(),
75
+ lvalue,
76
+ value,
77
+ };
78
+ self.wip.instructions.push(instr);
79
+ }
80
+
81
+ /// Terminates the work in progress block with the given terminal, and starts a new
82
+ /// work in progress block with the given kind
83
+ pub(crate) fn terminate(&mut self, terminal: TerminalValue<'a>, next_kind: BlockKind) {
84
+ let next_wip = WipBlock {
85
+ id: self.environment.next_block_id(),
86
+ kind: next_kind,
87
+ instructions: Vec::new_in(&self.environment.allocator),
88
+ };
89
+ let prev_wip = std::mem::replace(&mut self.wip, next_wip);
90
+ self.completed.insert(
91
+ prev_wip.id,
92
+ BasicBlock {
93
+ id: prev_wip.id,
94
+ kind: prev_wip.kind,
95
+ instructions: prev_wip.instructions,
96
+ terminal: Terminal {
97
+ id: self.id_gen.next(),
98
+ value: terminal,
99
+ },
100
+ predecessors: Default::default(),
101
+ },
102
+ );
103
+ }
104
+
105
+ /// Returns a new temporary identifier
106
+ pub(crate) fn make_temporary(&self) -> hir::Identifier<'a> {
107
+ hir::Identifier {
108
+ id: self.environment.next_identifier_id(),
109
+ mutable_range: Default::default(),
110
+ name: None,
111
+ scope: None,
112
+ type_: Type::Var(self.environment.next_type_var_id()),
113
+ }
114
+ }
115
+
116
+ /// Resolves the target for the given break label (if present), or returns the default
117
+ /// break target given the current context. Returns a diagnostic if the label is
118
+ /// provided but cannot be resolved.
119
+ pub(crate) fn resolve_break(&self, label: Option<Identifier>) -> Result<BlockId, Diagnostic> {
120
+ todo!()
121
+ }
122
+
123
+ /// Resolves the target for the given continue label (if present), or returns the default
124
+ /// continue target given the current context. Returns a diagnostic if the label is
125
+ /// provided but cannot be resolved.
126
+ pub(crate) fn resolve_continue(
127
+ &self,
128
+ label: Option<Identifier>,
129
+ ) -> Result<BlockId, Diagnostic> {
130
+ todo!()
131
+ }
132
}
133
134
/// Modifies the HIR to put the blocks in reverse postorder, with predecessors before
135
/// successors (except for the case of loops)
136
fn reverse_postorder_blocks<'a>(hir: &mut HIR<'a>) {
137
let mut visited = HashSet::<BlockId>::with_capacity(hir.blocks.len());
60
- let mut postorder = Vec::<BlockId>::with_capacity(hir.blocks.len());
138
+ let mut postorder = std::vec::Vec::<BlockId>::with_capacity(hir.blocks.len());
139
fn visit<'a>(
140
block_id: BlockId,
141
hir: &HIR<'a>,
142
visited: &mut HashSet<BlockId>,
65
- postorder: &mut Vec<BlockId>,
143
+ postorder: &mut std::vec::Vec<BlockId>,
144
) {
145
if !visited.insert(block_id) {
146
// already visited
@@ -98,6 +176,12 @@ fn reverse_postorder_blocks<'a>(hir: &mut HIR<'a>) {
176
hir.blocks = blocks;
177
}
178
179
+pub(crate) struct WipBlock<'a> {
180
+ pub id: BlockId,
181
+ pub kind: BlockKind,
182
+ pub instructions: Vec<'a, Instruction<'a>>,
183
+}
184
+
185
/// Prunes ForTerminal.update values (sets to None) if they are unreachable
186
fn remove_unreachable_for_updates<'a>(hir: &mut HIR<'a>) {
187
let block_ids: HashSet<BlockId> = hir.blocks.keys().cloned().collect();
compiler/forget/crates/hir/src/environment.rs
+1
-1
@@ -19,7 +19,7 @@ pub struct Environment<'a> {
19
20
/// Arena allocator so that data for compilation can be efficiently allocated
21
/// and the memory reclaimed when compilation completes.
22
- allocator: &'a Bump,
22
+ pub allocator: &'a Bump,
23
24
/// The next available block index
25
next_block_id: Cell<BlockId>,
compiler/forget/crates/hir/src/instruction.rs
+30
-1
@@ -31,7 +31,7 @@ pub enum InstructionValue<'a> {
31
// New(New<'a>),
32
// NextIterable(NextIterable<'a>),
33
// Object(Object<'a>),
34
- // Primitive(Primitive<'a>),
34
+ Primitive(Primitive<'a>),
35
// PropertyDelete(PropertyDelete<'a>),
36
// PropertyLoad(PropertyLoad<'a>),
37
// PropertyStore(PropertyStore<'a>),
@@ -54,6 +54,35 @@ pub enum ArrayElement<'a> {
54
Spread(Place<'a>),
55
}
56
57
+pub struct Primitive<'a> {
58
+ pub value: PrimitiveValue<'a>,
59
+}
60
+
61
+pub enum PrimitiveValue<'a> {
62
+ Boolean(bool),
63
+ Null,
64
+ Number(Number),
65
+ String(String<'a>),
66
+ Undefined,
67
+}
68
+
69
+/// Represents a JavaScript Number as its binary representation so that
70
+/// -1 == -1, NaN == Nan etc.
71
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
72
+pub struct Number(u64);
73
+
74
+impl From<f64> for Number {
75
+ fn from(value: f64) -> Self {
76
+ Self(value.to_bits())
77
+ }
78
+}
79
+
80
+impl From<Number> for f64 {
81
+ fn from(value: Number) -> Self {
82
+ f64::from_bits(value.0)
83
+ }
84
+}
85
+
86
pub struct LoadLocal<'a> {
87
pub place: Place<'a>,
88
}