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

[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 }