[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.
This commit is contained in:
Joe Savona
2023-07-06 09:24:44 +09:00
parent 2ac69bf7d9
commit b1c8cb358b
6 changed files with 305 additions and 12 deletions
+1
View File
@@ -113,6 +113,7 @@ dependencies = [
name = "build-hir"
version = "0.1.0"
dependencies = [
"bumpalo",
"estree",
"hir",
"indexmap 2.0.0",
@@ -9,3 +9,4 @@ edition = "2021"
hir = { path = "../hir" }
estree = { path = "../estree" }
indexmap = "2.0.0"
bumpalo = "3.13.0"
+185 -7
View File
@@ -1,5 +1,9 @@
use estree::FunctionDeclaration;
use hir::{Environment, Function};
use bumpalo::collections::{CollectIn, String};
use estree::{ExpressionLike, FunctionDeclaration, Literal, LiteralValue, Statement};
use hir::{
ArrayElement, BlockKind, Environment, Function, GotoKind, Identifier, InstructionValue,
LoadLocal, Place, PrimitiveValue, TerminalValue,
};
use crate::builder::Builder;
@@ -12,17 +16,191 @@ use crate::builder::Builder;
pub fn build<'a>(
environment: &'a Environment<'a>,
fun: FunctionDeclaration,
) -> Result<Function<'a>, Vec<()>> {
) -> Result<Function<'a>, Diagnostic> {
let mut builder = Builder::new(environment);
let body = match builder.build() {
Ok(body) => body,
Err(diagnostic) => return Err(vec![diagnostic]),
};
lower_statement(environment, &mut builder, fun.body.unwrap(), None)?;
// 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,
&mut builder,
InstructionValue::Primitive(hir::Primitive {
value: PrimitiveValue::Undefined,
}),
);
builder.terminate(
TerminalValue::ReturnTerminal(hir::ReturnTerminal {
value: implicit_return_value,
}),
hir::BlockKind::Block,
);
let body = builder.build()?;
Ok(Function {
body,
is_async: fun.is_async,
is_generator: fun.is_generator,
})
}
/// Convert a statement to HIR. This will often result in multiple instructions and blocks
/// being created as statements often describe control flow.
fn lower_statement<'a>(
env: &'a Environment<'a>,
builder: &mut Builder<'a>,
stmt: Statement,
label: Option<String<'a>>,
) -> Result<(), Diagnostic> {
match stmt {
Statement::BlockStatement(stmt) => {
for stmt in stmt.body {
lower_statement(env, builder, stmt, None)?;
}
}
Statement::BreakStatement(stmt) => {
let block = builder.resolve_break(stmt.label)?;
builder.terminate(
TerminalValue::GotoTerminal(hir::GotoTerminal {
block,
kind: GotoKind::Break,
}),
BlockKind::Block,
);
}
Statement::ContinueStatement(stmt) => {
let block = builder.resolve_continue(stmt.label)?;
builder.terminate(
TerminalValue::GotoTerminal(hir::GotoTerminal {
block,
kind: GotoKind::Continue,
}),
BlockKind::Block,
);
}
Statement::ReturnStatement(stmt) => {
let value = match stmt.argument {
Some(argument) => lower_expression_to_temporary(env, builder, argument),
None => lower_value_to_temporary(
env,
builder,
InstructionValue::Primitive(hir::Primitive {
value: PrimitiveValue::Undefined,
}),
),
};
builder.terminate(
TerminalValue::ReturnTerminal(hir::ReturnTerminal { value }),
BlockKind::Block,
);
}
Statement::ExpressionStatement(stmt) => {
// TODO: port the logic for emitting an ExpressionStatement instr if the instr
// was a logical or conditional. is that even necessary anymore?
lower_expression_to_temporary(env, builder, stmt.expression);
}
Statement::EmptyStatement(_) => {
// no-op
}
_ => todo!(),
}
Ok(())
}
/// Shortcut for lowering an expression and saving the result to a temporary
fn lower_expression_to_temporary<'a>(
env: &'a Environment<'a>,
builder: &mut Builder<'a>,
expr: ExpressionLike,
) -> Place<'a> {
let value = lower_expression(env, builder, expr);
lower_value_to_temporary(env, builder, value)
}
/// Converts an ESTree Expression into an HIR InstructionValue. Note that while only a single
/// InstructionValue is returned, this function is recursive and may cause multiple instructions
/// to be emitted, possibly across multiple basic blocks (in the case of expressions with control
/// flow semenatics such as logical, conditional, and optional expressions).
fn lower_expression<'a>(
env: &'a Environment<'a>,
builder: &mut Builder<'a>,
expr: ExpressionLike,
) -> InstructionValue<'a> {
match expr {
ExpressionLike::Literal(expr) => InstructionValue::Primitive(hir::Primitive {
value: lower_primitive(env, builder, *expr),
}),
ExpressionLike::ArrayExpression(expr) => {
let elements = expr
.elements
.into_iter()
.map(|expr| match expr {
ExpressionLike::SpreadElement(expr) => ArrayElement::Spread(
lower_expression_to_temporary(env, builder, expr.argument),
),
_ => ArrayElement::Place(lower_expression_to_temporary(env, builder, expr)),
})
.collect_in(env.allocator);
InstructionValue::Array(hir::Array { elements })
}
// Cases that cannot appear in expression position but which are included in ExpressionLike
// to make serialization easier
ExpressionLike::SpreadElement(_) => {
panic!("SpreadElement may not appear in normal expression position")
}
_ => todo!(),
}
}
/// Given an already lowered InstructionValue:
/// - if the instruction is a LoadLocal for a temporary location, avoid the indirection
/// and return the place that the LoadLocal loads from
/// - otherwise, create a new temporary place, push an instruction to associate the value with
/// that temporary, and return a clone of the temporary
fn lower_value_to_temporary<'a>(
env: &'a Environment<'a>,
builder: &mut Builder<'a>,
value: InstructionValue<'a>,
) -> Place<'a> {
if let InstructionValue::LoadLocal(LoadLocal {
place:
place @ Place {
identifier: Identifier { name: None, .. },
..
},
}) = value
{
return place;
}
let place = build_temporary_place(env, builder);
builder.push(todo!("clone `place`"), value);
return place;
}
/// Constructs a temporary Identifier and Place wrapper, which can be used as an Instruction lvalue
/// or other places where a temporary target is required
fn build_temporary_place<'a>(env: &'a Environment<'a>, builder: &mut Builder<'a>) -> Place<'a> {
Place {
identifier: builder.make_temporary(),
effect: None,
}
}
/// Converts an ESTree literal into a HIR primitive
fn lower_primitive<'a>(
env: &'a Environment<'a>,
_builder: &mut Builder<'a>,
literal: Literal,
) -> PrimitiveValue<'a> {
match literal.value {
LiteralValue::Boolean(bool) => PrimitiveValue::Boolean(bool),
LiteralValue::Null => PrimitiveValue::Null,
LiteralValue::Number(value) => PrimitiveValue::Number(f64::from(value).into()),
LiteralValue::String(s) => PrimitiveValue::String(String::from_str_in(&s, &env.allocator)),
_ => todo!(),
}
}
type Diagnostic = ();
@@ -1,6 +1,11 @@
use bumpalo::collections::Vec;
use estree::Identifier;
use std::collections::HashSet;
use hir::{BasicBlock, BlockId, Environment, GotoKind, InstructionIdGenerator, TerminalValue, HIR};
use hir::{
BasicBlock, BlockId, BlockKind, Environment, GotoKind, Instruction, InstructionIdGenerator,
InstructionValue, Place, Terminal, TerminalValue, Type, HIR,
};
use indexmap::IndexMap;
/// Helper struct used when converting from ESTree to HIR. Includes:
@@ -19,15 +24,26 @@ pub struct Builder<'a> {
completed: IndexMap<BlockId, BasicBlock<'a>>,
entry: BlockId,
wip: WipBlock<'a>,
id_gen: InstructionIdGenerator,
}
impl<'a> Builder<'a> {
pub(crate) fn new(environment: &'a Environment<'a>) -> Self {
let entry = environment.next_block_id();
let current = WipBlock {
id: entry,
kind: BlockKind::Block,
instructions: Vec::new_in(&environment.allocator),
};
Self {
environment,
completed: Default::default(),
entry,
wip: current,
id_gen: InstructionIdGenerator::new(),
}
}
@@ -51,18 +67,80 @@ impl<'a> Builder<'a> {
Ok(hir)
}
/// Adds a new instruction to the end of the work in progress block
pub(crate) fn push(&mut self, lvalue: Place<'a>, value: InstructionValue<'a>) {
let instr = Instruction {
id: self.id_gen.next(),
lvalue,
value,
};
self.wip.instructions.push(instr);
}
/// Terminates the work in progress block with the given terminal, and starts a new
/// work in progress block with the given kind
pub(crate) fn terminate(&mut self, terminal: TerminalValue<'a>, next_kind: BlockKind) {
let next_wip = WipBlock {
id: self.environment.next_block_id(),
kind: next_kind,
instructions: Vec::new_in(&self.environment.allocator),
};
let prev_wip = std::mem::replace(&mut self.wip, next_wip);
self.completed.insert(
prev_wip.id,
BasicBlock {
id: prev_wip.id,
kind: prev_wip.kind,
instructions: prev_wip.instructions,
terminal: Terminal {
id: self.id_gen.next(),
value: terminal,
},
predecessors: Default::default(),
},
);
}
/// Returns a new temporary identifier
pub(crate) fn make_temporary(&self) -> hir::Identifier<'a> {
hir::Identifier {
id: self.environment.next_identifier_id(),
mutable_range: Default::default(),
name: None,
scope: None,
type_: Type::Var(self.environment.next_type_var_id()),
}
}
/// Resolves the target for the given break label (if present), or returns the default
/// break target given the current context. Returns a diagnostic if the label is
/// provided but cannot be resolved.
pub(crate) fn resolve_break(&self, label: Option<Identifier>) -> Result<BlockId, Diagnostic> {
todo!()
}
/// Resolves the target for the given continue label (if present), or returns the default
/// continue target given the current context. Returns a diagnostic if the label is
/// provided but cannot be resolved.
pub(crate) fn resolve_continue(
&self,
label: Option<Identifier>,
) -> Result<BlockId, Diagnostic> {
todo!()
}
}
/// 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>) {
let mut visited = HashSet::<BlockId>::with_capacity(hir.blocks.len());
let mut postorder = Vec::<BlockId>::with_capacity(hir.blocks.len());
let mut postorder = std::vec::Vec::<BlockId>::with_capacity(hir.blocks.len());
fn visit<'a>(
block_id: BlockId,
hir: &HIR<'a>,
visited: &mut HashSet<BlockId>,
postorder: &mut Vec<BlockId>,
postorder: &mut std::vec::Vec<BlockId>,
) {
if !visited.insert(block_id) {
// already visited
@@ -98,6 +176,12 @@ fn reverse_postorder_blocks<'a>(hir: &mut HIR<'a>) {
hir.blocks = blocks;
}
pub(crate) struct WipBlock<'a> {
pub id: BlockId,
pub kind: BlockKind,
pub instructions: Vec<'a, Instruction<'a>>,
}
/// Prunes ForTerminal.update values (sets to None) if they are unreachable
fn remove_unreachable_for_updates<'a>(hir: &mut HIR<'a>) {
let block_ids: HashSet<BlockId> = hir.blocks.keys().cloned().collect();
@@ -19,7 +19,7 @@ pub struct Environment<'a> {
/// Arena allocator so that data for compilation can be efficiently allocated
/// and the memory reclaimed when compilation completes.
allocator: &'a Bump,
pub allocator: &'a Bump,
/// The next available block index
next_block_id: Cell<BlockId>,
+30 -1
View File
@@ -31,7 +31,7 @@ pub enum InstructionValue<'a> {
// New(New<'a>),
// NextIterable(NextIterable<'a>),
// Object(Object<'a>),
// Primitive(Primitive<'a>),
Primitive(Primitive<'a>),
// PropertyDelete(PropertyDelete<'a>),
// PropertyLoad(PropertyLoad<'a>),
// PropertyStore(PropertyStore<'a>),
@@ -54,6 +54,35 @@ pub enum ArrayElement<'a> {
Spread(Place<'a>),
}
pub struct Primitive<'a> {
pub value: PrimitiveValue<'a>,
}
pub enum PrimitiveValue<'a> {
Boolean(bool),
Null,
Number(Number),
String(String<'a>),
Undefined,
}
/// Represents a JavaScript Number as its binary representation so that
/// -1 == -1, NaN == Nan etc.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct Number(u64);
impl From<f64> for Number {
fn from(value: f64) -> Self {
Self(value.to_bits())
}
}
impl From<Number> for f64 {
fn from(value: Number) -> Self {
f64::from_bits(value.0)
}
}
pub struct LoadLocal<'a> {
pub place: Place<'a>,
}