Browse Source

Implement register allocation (#3942)

* Implement register allocation

* Apply review

* Update core/engine/src/vm/opcode/rest_parameter/mod.rs

Co-authored-by: raskad <32105367+raskad@users.noreply.github.com>

---------

Co-authored-by: raskad <32105367+raskad@users.noreply.github.com>
pull/3997/head
Haled Odat 3 months ago committed by GitHub
parent
commit
c480a42230
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      core/engine/src/builtins/array/mod.rs
  2. 2
      core/engine/src/builtins/eval/mod.rs
  3. 2
      core/engine/src/builtins/json/mod.rs
  4. 10
      core/engine/src/bytecompiler/class.rs
  5. 8
      core/engine/src/bytecompiler/function.rs
  6. 71
      core/engine/src/bytecompiler/mod.rs
  7. 140
      core/engine/src/bytecompiler/register.rs
  8. 5
      core/engine/src/module/source.rs
  9. 2
      core/engine/src/module/synthetic.rs
  10. 2
      core/engine/src/script.rs
  11. 1
      core/engine/src/vm/call_frame/mod.rs
  12. 6
      core/engine/src/vm/code_block.rs
  13. 6
      core/engine/src/vm/flowgraph/mod.rs
  14. 73
      core/engine/src/vm/opcode/control_flow/return.rs
  15. 18
      core/engine/src/vm/opcode/mod.rs
  16. 26
      core/engine/src/vm/opcode/rest_parameter/mod.rs

2
core/engine/src/builtins/array/mod.rs

@ -382,7 +382,7 @@ impl Array {
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createarrayfromlist
pub(crate) fn create_array_from_list<I>(elements: I, context: &mut Context) -> JsObject
pub(crate) fn create_array_from_list<I>(elements: I, context: &Context) -> JsObject
where
I: IntoIterator<Item = JsValue>,
{

2
core/engine/src/builtins/eval/mod.rs

@ -254,6 +254,8 @@ impl Eval {
false,
var_env.clone(),
lex_env.clone(),
false,
false,
context.interner_mut(),
in_with,
);

2
core/engine/src/builtins/json/mod.rs

@ -120,6 +120,8 @@ impl Json {
true,
context.realm().environment().compile_env(),
context.realm().environment().compile_env(),
false,
false,
context.interner_mut(),
in_with,
);

10
core/engine/src/bytecompiler/class.rs

@ -92,6 +92,8 @@ impl ByteCompiler<'_> {
self.json_parse,
self.variable_environment.clone(),
self.lexical_environment.clone(),
false,
false,
self.interner,
self.in_with,
);
@ -274,6 +276,8 @@ impl ByteCompiler<'_> {
self.json_parse,
self.variable_environment.clone(),
self.lexical_environment.clone(),
false,
false,
self.interner,
self.in_with,
);
@ -308,6 +312,8 @@ impl ByteCompiler<'_> {
self.json_parse,
self.variable_environment.clone(),
self.lexical_environment.clone(),
false,
false,
self.interner,
self.in_with,
);
@ -347,6 +353,8 @@ impl ByteCompiler<'_> {
self.json_parse,
self.variable_environment.clone(),
self.lexical_environment.clone(),
false,
false,
self.interner,
self.in_with,
);
@ -388,6 +396,8 @@ impl ByteCompiler<'_> {
false,
self.variable_environment.clone(),
self.lexical_environment.clone(),
false,
false,
self.interner,
self.in_with,
);

8
core/engine/src/bytecompiler/function.rs

@ -112,16 +112,12 @@ impl FunctionCompiler {
false,
variable_environment,
lexical_environment,
self.r#async,
self.generator,
interner,
self.in_with,
);
compiler.length = length;
compiler
.code_block_flags
.set(CodeBlockFlags::IS_ASYNC, self.r#async);
compiler
.code_block_flags
.set(CodeBlockFlags::IS_GENERATOR, self.generator);
compiler.code_block_flags.set(
CodeBlockFlags::HAS_PROTOTYPE_PROPERTY,
!self.arrow && !self.method && !self.r#async && !self.generator,

71
core/engine/src/bytecompiler/mod.rs

@ -8,6 +8,7 @@ mod expression;
mod function;
mod jump_control;
mod module;
mod register;
mod statement;
mod utils;
@ -18,8 +19,8 @@ use crate::{
environments::{BindingLocator, BindingLocatorError, CompileTimeEnvironment},
js_string,
vm::{
BindingOpcode, CodeBlock, CodeBlockFlags, Constant, GeneratorResumeKind, Handler,
InlineCache, Opcode, VaryingOperandKind,
BindingOpcode, CallFrame, CodeBlock, CodeBlockFlags, Constant, GeneratorResumeKind,
Handler, InlineCache, Opcode, VaryingOperandKind,
},
JsBigInt, JsStr, JsString,
};
@ -54,6 +55,7 @@ pub(crate) use declarations::{
};
pub(crate) use function::FunctionCompiler;
pub(crate) use jump_control::JumpControlInfo;
pub(crate) use register::*;
pub(crate) trait ToJsString {
fn to_js_string(&self, interner: &Interner) -> JsString;
@ -384,7 +386,7 @@ pub struct ByteCompiler<'ctx> {
/// The number of arguments expected.
pub(crate) length: u32,
pub(crate) register_count: u32,
pub(crate) register_allocator: RegisterAllocator,
/// `[[ThisMode]]`
pub(crate) this_mode: ThisMode,
@ -408,7 +410,7 @@ pub struct ByteCompiler<'ctx> {
pub(crate) current_open_environments_count: u32,
current_stack_value_count: u32,
pub(crate) code_block_flags: CodeBlockFlags,
code_block_flags: CodeBlockFlags,
handlers: ThinVec<Handler>,
pub(crate) ic: Vec<InlineCache>,
literals_map: FxHashMap<Literal, u32>,
@ -441,18 +443,53 @@ impl<'ctx> ByteCompiler<'ctx> {
/// Creates a new [`ByteCompiler`].
#[inline]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) fn new(
name: JsString,
strict: bool,
json_parse: bool,
variable_environment: Rc<CompileTimeEnvironment>,
lexical_environment: Rc<CompileTimeEnvironment>,
is_async: bool,
is_generator: bool,
interner: &'ctx mut Interner,
in_with: bool,
) -> ByteCompiler<'ctx> {
let mut code_block_flags = CodeBlockFlags::empty();
code_block_flags.set(CodeBlockFlags::STRICT, strict);
code_block_flags.set(CodeBlockFlags::IS_ASYNC, is_async);
code_block_flags.set(CodeBlockFlags::IS_GENERATOR, is_generator);
code_block_flags |= CodeBlockFlags::HAS_PROTOTYPE_PROPERTY;
let mut register_allocator = RegisterAllocator::default();
if is_async {
let promise_register = register_allocator.alloc_persistent();
let resolve_register = register_allocator.alloc_persistent();
let reject_register = register_allocator.alloc_persistent();
debug_assert_eq!(
promise_register.index(),
CallFrame::PROMISE_CAPABILITY_PROMISE_REGISTER_INDEX
);
debug_assert_eq!(
resolve_register.index(),
CallFrame::PROMISE_CAPABILITY_RESOLVE_REGISTER_INDEX
);
debug_assert_eq!(
reject_register.index(),
CallFrame::PROMISE_CAPABILITY_REJECT_REGISTER_INDEX
);
if is_generator {
let async_function_object_register = register_allocator.alloc_persistent();
debug_assert_eq!(
async_function_object_register.index(),
CallFrame::ASYNC_GENERATOR_OBJECT_REGISTER_INDEX
);
}
}
Self {
function_name: name,
length: 0,
@ -463,7 +500,7 @@ impl<'ctx> ByteCompiler<'ctx> {
params: FormalParameterList::default(),
current_open_environments_count: 0,
register_count: 0,
register_allocator,
current_stack_value_count: 0,
code_block_flags,
handlers: ThinVec::default(),
@ -640,6 +677,17 @@ impl<'ctx> ByteCompiler<'ctx> {
}
}
/// TODO: Temporary function, remove once transition is complete.
#[allow(unused)]
fn pop_into_register(&mut self, dst: &Register) {
self.emit(Opcode::PopIntoRegister, &[Operand::Varying(dst.index())]);
}
/// TODO: Temporary function, remove once transition is complete.
#[allow(unused)]
fn push_from_register(&mut self, src: &Register) {
self.emit(Opcode::PushFromRegister, &[Operand::Varying(src.index())]);
}
/// Emits an opcode with one varying operand.
///
/// Simpler version of [`ByteCompiler::emit()`].
@ -1689,19 +1737,12 @@ impl<'ctx> ByteCompiler<'ctx> {
}
self.r#return(false);
if self.is_async() {
// NOTE: +3 for the promise capability
self.register_count += 3;
if self.is_generator() {
// NOTE: +1 for the async generator function
self.register_count += 1;
}
}
let register_count = self.register_allocator.finish();
// NOTE: Offset the handlers stack count so we don't pop the registers
// when a exception is thrown.
for handler in &mut self.handlers {
handler.stack_count += self.register_count;
handler.stack_count += register_count;
}
let mapped_arguments_binding_indices = if self.emitted_mapped_arguments_object_opcode {
@ -1713,7 +1754,7 @@ impl<'ctx> ByteCompiler<'ctx> {
CodeBlock {
name: self.function_name,
length: self.length,
register_count: self.register_count,
register_count,
this_mode: self.this_mode,
parameter_length: self.params.as_ref().len() as u32,
mapped_arguments_binding_indices,

140
core/engine/src/bytecompiler/register.rs

@ -0,0 +1,140 @@
use std::mem::forget;
bitflags::bitflags! {
#[derive(Debug, Default, Clone, Copy)]
struct RegisterFlags: u8 {
/// Whether the register is still in use (not deallocated).
const USED = 0b0000_0001;
/// Is the register presistent (not deallocatable).
const PERSISTENT = 0b0000_0010;
}
}
impl RegisterFlags {
fn is_used(self) -> bool {
self.contains(Self::USED)
}
fn is_persistent(self) -> bool {
self.contains(Self::PERSISTENT)
}
}
/// An entry in the [`RegisterAllocator`].
#[derive(Debug, Default)]
pub(crate) struct RegisterEntry {
flags: RegisterFlags,
}
/// Represent a VM register.
///
/// This is intented to be passed by reference or to be moved, dropping this is a bug,
/// it should only be dropped though the [`RegisterAllocator::dealloc()`] method.
/// This doesn't apply to persistent registers.
///
/// A [`Register`] is index into the register allocator,
/// as well as an index into the registers on the stack using the register pointer (`rp`).
#[derive(Debug)]
pub(crate) struct Register {
index: u32,
flags: RegisterFlags,
}
impl Register {
/// The index of the [`Register`].
pub(crate) fn index(&self) -> u32 {
self.index
}
}
impl Drop for Register {
/// This method should never be called.
/// It is used to detect when a register has not been deallocated.
fn drop(&mut self) {
if self.flags.is_persistent() {
return;
}
// Prevent double panic.
if std::thread::panicking() {
return;
}
unreachable!("forgot to deallocate a register!")
}
}
#[derive(Debug, Default)]
pub(crate) struct RegisterAllocator {
registers: Vec<RegisterEntry>,
}
impl RegisterAllocator {
pub(crate) fn alloc(&mut self) -> Register {
if let Some((i, register)) = self
.registers
.iter_mut()
.filter(|reg| !reg.flags.is_used())
.enumerate()
.next()
{
assert!(!register.flags.is_persistent());
register.flags |= RegisterFlags::USED;
return Register {
index: i as u32,
flags: register.flags,
};
}
let flags = RegisterFlags::USED;
let index = self.registers.len() as u32;
self.registers.push(RegisterEntry { flags });
Register { index, flags }
}
pub(crate) fn alloc_persistent(&mut self) -> Register {
let mut reg = self.alloc();
let index = reg.index();
let register = &mut self.registers[index as usize];
register.flags |= RegisterFlags::PERSISTENT;
reg.flags = register.flags;
reg
}
#[allow(unused)]
pub(crate) fn dealloc(&mut self, reg: Register) {
assert!(
!reg.flags.is_persistent(),
"Trying to deallocate a persistent register"
);
let register = &mut self.registers[reg.index as usize];
assert!(
register.flags.is_used(),
"Cannot deallocate unused variable"
);
register.flags.set(RegisterFlags::USED, false);
// NOTE: We should not drop it since, dropping it used to detect bugs.
forget(reg);
}
pub(crate) fn finish(self) -> u32 {
for register in &self.registers {
debug_assert!(
!register.flags.is_used()
|| (register.flags.is_used() && register.flags.is_persistent())
);
}
self.registers.len() as u32
}
}

5
core/engine/src/module/source.rs

@ -28,7 +28,7 @@ use crate::{
realm::Realm,
vm::{
create_function_object_fast, ActiveRunnable, CallFrame, CallFrameFlags, CodeBlock,
CodeBlockFlags, CompletionRecord, Opcode,
CompletionRecord, Opcode,
},
Context, JsArgs, JsError, JsNativeError, JsObject, JsResult, JsString, JsValue, NativeFunction,
};
@ -1432,11 +1432,12 @@ impl SourceTextModule {
false,
env.clone(),
env.clone(),
true,
false,
context.interner_mut(),
false,
);
compiler.code_block_flags |= CodeBlockFlags::IS_ASYNC;
compiler.async_handler = Some(compiler.push_handler());
let mut imports = Vec::new();

2
core/engine/src/module/synthetic.rs

@ -286,6 +286,8 @@ impl SyntheticModule {
false,
module_compile_env.clone(),
module_compile_env.clone(),
false,
false,
context.interner_mut(),
false,
);

2
core/engine/src/script.rs

@ -134,6 +134,8 @@ impl Script {
false,
self.inner.realm.environment().compile_env(),
self.inner.realm.environment().compile_env(),
false,
false,
context.interner_mut(),
false,
);

1
core/engine/src/vm/call_frame/mod.rs

@ -43,6 +43,7 @@ pub struct CallFrame {
pub(crate) code_block: Gc<CodeBlock>,
pub(crate) pc: u32,
/// The register pointer, points to the first register in the stack.
///
// TODO: Check if storing the frame pointer instead of argument count and computing the
// argument count based on the pointers would be better for accessing the arguments
// and the elements before the register pointer.

6
core/engine/src/vm/code_block.rs

@ -549,6 +549,8 @@ impl CodeBlock {
} => {
format!("is_anonymous_function: {is_anonymous_function}")
}
Instruction::PopIntoRegister { dst } => format!("dst:reg{}", dst.value()),
Instruction::PushFromRegister { src } => format!("src:reg{}", src.value()),
Instruction::Pop
| Instruction::Dup
| Instruction::Swap
@ -724,9 +726,7 @@ impl CodeBlock {
| Instruction::Reserved48
| Instruction::Reserved49
| Instruction::Reserved50
| Instruction::Reserved51
| Instruction::Reserved52
| Instruction::Reserved53 => unreachable!("Reserved opcodes are unrechable"),
| Instruction::Reserved51 => unreachable!("Reserved opcodes are unrechable"),
}
}
}

6
core/engine/src/vm/flowgraph/mod.rs

@ -455,6 +455,8 @@ impl CodeBlock {
| Instruction::CreateUnmappedArgumentsObject
| Instruction::CreateGlobalFunctionBinding { .. }
| Instruction::CreateGlobalVarBinding { .. }
| Instruction::PopIntoRegister { .. }
| Instruction::PushFromRegister { .. }
| Instruction::Nop => {
graph.add_node(previous_pc, NodeShape::None, label.into(), Color::None);
graph.add_edge(previous_pc, pc, None, Color::None, EdgeStyle::Line);
@ -514,9 +516,7 @@ impl CodeBlock {
| Instruction::Reserved48
| Instruction::Reserved49
| Instruction::Reserved50
| Instruction::Reserved51
| Instruction::Reserved52
| Instruction::Reserved53 => unreachable!("Reserved opcodes are unrechable"),
| Instruction::Reserved51 => unreachable!("Reserved opcodes are unrechable"),
}
}

73
core/engine/src/vm/opcode/control_flow/return.rs

@ -114,3 +114,76 @@ impl Operation for SetReturnValue {
Ok(CompletionType::Normal)
}
}
/// TODO: doc
#[derive(Debug, Clone, Copy)]
pub(crate) struct PopIntoRegister;
impl PopIntoRegister {
#[allow(clippy::unnecessary_wraps)]
#[allow(clippy::needless_pass_by_value)]
fn operation(dst: u32, context: &mut Context) -> JsResult<CompletionType> {
let value = context.vm.pop();
let rp = context.vm.frame().rp;
context.vm.stack[(rp + dst) as usize] = value;
Ok(CompletionType::Normal)
}
}
impl Operation for PopIntoRegister {
const NAME: &'static str = "PopIntoRegister";
const INSTRUCTION: &'static str = "INST - PopIntoRegister";
const COST: u8 = 2;
fn execute(context: &mut Context) -> JsResult<CompletionType> {
let dst = u32::from(context.vm.read::<u8>());
Self::operation(dst, context)
}
fn execute_with_u16_operands(context: &mut Context) -> JsResult<CompletionType> {
let dst = u32::from(context.vm.read::<u16>());
Self::operation(dst, context)
}
fn execute_with_u32_operands(context: &mut Context) -> JsResult<CompletionType> {
let dst = context.vm.read::<u32>();
Self::operation(dst, context)
}
}
/// TODO: doc
#[derive(Debug, Clone, Copy)]
pub(crate) struct PushFromRegister;
impl PushFromRegister {
#[allow(clippy::unnecessary_wraps)]
#[allow(clippy::needless_pass_by_value)]
fn operation(dst: u32, context: &mut Context) -> JsResult<CompletionType> {
let rp = context.vm.frame().rp;
let value = context.vm.stack[(rp + dst) as usize].clone();
context.vm.push(value);
Ok(CompletionType::Normal)
}
}
impl Operation for PushFromRegister {
const NAME: &'static str = "PushFromRegister";
const INSTRUCTION: &'static str = "INST - PushFromRegister";
const COST: u8 = 2;
fn execute(context: &mut Context) -> JsResult<CompletionType> {
let dst = u32::from(context.vm.read::<u8>());
Self::operation(dst, context)
}
fn execute_with_u16_operands(context: &mut Context) -> JsResult<CompletionType> {
let dst = u32::from(context.vm.read::<u16>());
Self::operation(dst, context)
}
fn execute_with_u32_operands(context: &mut Context) -> JsResult<CompletionType> {
let dst = context.vm.read::<u32>();
Self::operation(dst, context)
}
}

18
core/engine/src/vm/opcode/mod.rs

@ -1780,6 +1780,20 @@ generate_opcodes! {
/// Stack: value **=>**
SetReturnValue,
/// Pop value from the stack and push to register `dst`
///
/// Operands:
///
/// Stack: value **=>**
PopIntoRegister { dst: VaryingOperand },
/// Copy value at register `src` and push it into the stack.
///
/// Operands:
///
/// Stack: **=>** value
PushFromRegister { src: VaryingOperand },
/// Push a declarative environment.
///
/// Operands: compile_environments_index: `VaryingOperand`
@ -2255,10 +2269,6 @@ generate_opcodes! {
Reserved50 => Reserved,
/// Reserved [`Opcode`].
Reserved51 => Reserved,
/// Reserved [`Opcode`].
Reserved52 => Reserved,
/// Reserved [`Opcode`].
Reserved53 => Reserved,
}
/// Specific opcodes for bindings.

26
core/engine/src/vm/opcode/rest_parameter/mod.rs

@ -20,16 +20,30 @@ impl Operation for RestParameterInit {
let frame = context.vm.frame();
let argument_count = frame.argument_count;
let param_count = frame.code_block().parameter_length;
let register_count = frame.code_block().register_count;
let array = if argument_count >= param_count {
if argument_count >= param_count {
let rest_count = argument_count - param_count + 1;
let args = context.vm.pop_n_values(rest_count as usize);
Array::create_array_from_list(args, context)
} else {
Array::array_create(0, None, context).expect("could not create an empty array")
};
let len = context.vm.stack.len() as u32;
let start = (len - rest_count - register_count) as usize;
let end = (len - register_count) as usize;
let args = &context.vm.stack[start..end];
let array = Array::create_array_from_list(args.iter().cloned(), context);
context.vm.stack.drain(start..end);
context.vm.frame_mut().rp -= (start..end).len() as u32;
context.vm.frame_mut().argument_count -= (start..end).len() as u32;
context.vm.push(array);
} else {
let array =
Array::array_create(0, None, context).expect("could not create an empty array");
context.vm.push(array);
}
Ok(CompletionType::Normal)
}
}

Loading…
Cancel
Save