From b1c8cb358b913564c83c64bb579e460a1e0bef43 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Thu, 6 Jul 2023 09:24:44 +0900 Subject: [PATCH] [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. --- compiler/forget/Cargo.lock | 1 + compiler/forget/crates/build-hir/Cargo.toml | 1 + compiler/forget/crates/build-hir/src/build.rs | 192 +++++++++++++++++- .../forget/crates/build-hir/src/builder.rs | 90 +++++++- compiler/forget/crates/hir/src/environment.rs | 2 +- compiler/forget/crates/hir/src/instruction.rs | 31 ++- 6 files changed, 305 insertions(+), 12 deletions(-) diff --git a/compiler/forget/Cargo.lock b/compiler/forget/Cargo.lock index f1a47e20fb..19c724edf3 100644 --- a/compiler/forget/Cargo.lock +++ b/compiler/forget/Cargo.lock @@ -113,6 +113,7 @@ dependencies = [ name = "build-hir" version = "0.1.0" dependencies = [ + "bumpalo", "estree", "hir", "indexmap 2.0.0", diff --git a/compiler/forget/crates/build-hir/Cargo.toml b/compiler/forget/crates/build-hir/Cargo.toml index 42a3140f51..5e108f3706 100644 --- a/compiler/forget/crates/build-hir/Cargo.toml +++ b/compiler/forget/crates/build-hir/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" hir = { path = "../hir" } estree = { path = "../estree" } indexmap = "2.0.0" +bumpalo = "3.13.0" diff --git a/compiler/forget/crates/build-hir/src/build.rs b/compiler/forget/crates/build-hir/src/build.rs index a3c860414a..0da854c700 100644 --- a/compiler/forget/crates/build-hir/src/build.rs +++ b/compiler/forget/crates/build-hir/src/build.rs @@ -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, Vec<()>> { +) -> Result, 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>, +) -> 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 = (); diff --git a/compiler/forget/crates/build-hir/src/builder.rs b/compiler/forget/crates/build-hir/src/builder.rs index 63fdb092a7..48daf934cd 100644 --- a/compiler/forget/crates/build-hir/src/builder.rs +++ b/compiler/forget/crates/build-hir/src/builder.rs @@ -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>, 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) -> Result { + 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, + ) -> Result { + 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::::with_capacity(hir.blocks.len()); - let mut postorder = Vec::::with_capacity(hir.blocks.len()); + let mut postorder = std::vec::Vec::::with_capacity(hir.blocks.len()); fn visit<'a>( block_id: BlockId, hir: &HIR<'a>, visited: &mut HashSet, - postorder: &mut Vec, + postorder: &mut std::vec::Vec, ) { 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 = hir.blocks.keys().cloned().collect(); diff --git a/compiler/forget/crates/hir/src/environment.rs b/compiler/forget/crates/hir/src/environment.rs index 357a9a3b13..16f07c4301 100644 --- a/compiler/forget/crates/hir/src/environment.rs +++ b/compiler/forget/crates/hir/src/environment.rs @@ -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, diff --git a/compiler/forget/crates/hir/src/instruction.rs b/compiler/forget/crates/hir/src/instruction.rs index 8f56abde91..a0e5859831 100644 --- a/compiler/forget/crates/hir/src/instruction.rs +++ b/compiler/forget/crates/hir/src/instruction.rs @@ -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 for Number { + fn from(value: f64) -> Self { + Self(value.to_bits()) + } +} + +impl From for f64 { + fn from(value: Number) -> Self { + f64::from_bits(value.0) + } +} + pub struct LoadLocal<'a> { pub place: Place<'a>, }