LibJS/Bytecode: Flatten bytecode to a contiguous representation

Instead of keeping bytecode as a set of disjoint basic blocks on the
malloc heap, bytecode is now a contiguous sequence of bytes(!)

The transformation happens at the end of Bytecode::Generator::generate()
and the only really hairy part is rerouting jump labels.

This required solving a few problems:

- The interpreter execution loop had to change quite a bit, since we
  were storing BasicBlock pointers all over the place, and control
  transfer was done by redirecting the interpreter's current block.

- Exception handlers & finalizers are now stored per-bytecode-range
  in a side table in Executable.

- The interpreter now has a plain program counter instead of a stream
  iterator. This actually makes error stack generation a bit nicer
  since we just have to deal with a number instead of reaching into
  the iterator.

This yields a 25% performance improvement on this microbenchmark:

    for (let i = 0; i < 1_000_000; ++i) { }

But basically everything gets faster. :^)
This commit is contained in:
Andreas Kling 2024-05-06 06:44:08 +02:00
commit f6aee2b9e8
Notes: sideshowbarker 2024-07-17 02:22:23 +09:00
21 changed files with 392 additions and 172 deletions

View file

@ -30,27 +30,6 @@ BasicBlock::~BasicBlock()
}
}
void BasicBlock::dump(Bytecode::Executable const& executable) const
{
Bytecode::InstructionStreamIterator it(instruction_stream());
if (!m_name.is_empty())
warn("{}", m_name);
if (m_handler || m_finalizer) {
warn(" [");
if (m_handler)
warn(" Handler: {}", Label { *m_handler });
if (m_finalizer)
warn(" Finalizer: {}", Label { *m_finalizer });
warn(" ]");
}
warnln(":");
while (!it.at_end()) {
warnln("[{:4x}] {}", it.offset(), (*it).to_byte_string(executable));
++it;
}
}
void BasicBlock::grow(size_t additional_size)
{
m_buffer.grow_capacity(m_buffer.size() + additional_size);

View file

@ -27,7 +27,6 @@ public:
static NonnullOwnPtr<BasicBlock> create(String name);
~BasicBlock();
void dump(Executable const&) const;
ReadonlyBytes instruction_stream() const { return m_buffer.span(); }
u8* data() { return m_buffer.data(); }
u8 const* data() const { return m_buffer.data(); }

View file

@ -1,11 +1,12 @@
/*
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021-2024, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Bytecode/BasicBlock.h>
#include <LibJS/Bytecode/Executable.h>
#include <LibJS/Bytecode/Instruction.h>
#include <LibJS/Bytecode/RegexTable.h>
#include <LibJS/SourceCode.h>
@ -14,6 +15,7 @@ namespace JS::Bytecode {
JS_DEFINE_ALLOCATOR(Executable);
Executable::Executable(
Vector<u8> bytecode,
NonnullOwnPtr<IdentifierTable> identifier_table,
NonnullOwnPtr<StringTable> string_table,
NonnullOwnPtr<RegexTable> regex_table,
@ -23,9 +25,8 @@ Executable::Executable(
size_t number_of_global_variable_caches,
size_t number_of_environment_variable_caches,
size_t number_of_registers,
Vector<NonnullOwnPtr<BasicBlock>> basic_blocks,
bool is_strict_mode)
: basic_blocks(move(basic_blocks))
: bytecode(move(bytecode))
, string_table(move(string_table))
, identifier_table(move(identifier_table))
, regex_table(move(regex_table))
@ -44,8 +45,42 @@ Executable::~Executable() = default;
void Executable::dump() const
{
warnln("\033[37;1mJS bytecode executable\033[0m \"{}\"", name);
for (auto& block : basic_blocks)
block->dump(*this);
InstructionStreamIterator it(bytecode, this);
size_t basic_block_offset_index = 0;
while (!it.at_end()) {
bool print_basic_block_marker = false;
if (basic_block_offset_index < basic_block_start_offsets.size()
&& it.offset() == basic_block_start_offsets[basic_block_offset_index]) {
++basic_block_offset_index;
print_basic_block_marker = true;
}
StringBuilder builder;
builder.appendff("[{:4x}] ", it.offset());
if (print_basic_block_marker)
builder.appendff("{:4}: ", basic_block_offset_index - 1);
else
builder.append(" "sv);
builder.append((*it).to_byte_string(*this));
warnln("{}", builder.string_view());
++it;
}
if (!exception_handlers.is_empty()) {
warnln("");
warnln("Exception handlers:");
for (auto& handlers : exception_handlers) {
warnln(" from {:4x} to {:4x} handler {:4x} finalizer {:4x}",
handlers.start_offset,
handlers.end_offset,
handlers.handler_offset.value_or(0),
handlers.finalizer_offset.value_or(0));
}
}
warnln("");
}
@ -56,4 +91,27 @@ void Executable::visit_edges(Visitor& visitor)
visitor.visit(constants);
}
Optional<Executable::ExceptionHandlers const&> Executable::exception_handlers_for_offset(size_t offset) const
{
for (auto& handlers : exception_handlers) {
if (handlers.start_offset <= offset && offset < handlers.end_offset)
return handlers;
}
return {};
}
UnrealizedSourceRange Executable::source_range_at(size_t offset) const
{
if (offset >= bytecode.size())
return {};
auto it = InstructionStreamIterator(bytecode.span().slice(offset), this);
VERIFY(!it.at_end());
auto& instruction = *it;
return UnrealizedSourceRange {
.source_code = source_code,
.start_offset = instruction.source_record().source_start_offset,
.end_offset = instruction.source_record().source_end_offset,
};
}
}

View file

@ -18,6 +18,7 @@
#include <LibJS/Heap/Cell.h>
#include <LibJS/Heap/CellAllocator.h>
#include <LibJS/Runtime/EnvironmentCoordinate.h>
#include <LibJS/SourceRange.h>
namespace JS::Bytecode {
@ -45,6 +46,7 @@ class Executable final : public Cell {
public:
Executable(
Vector<u8> bytecode,
NonnullOwnPtr<IdentifierTable>,
NonnullOwnPtr<StringTable>,
NonnullOwnPtr<RegexTable>,
@ -54,16 +56,15 @@ public:
size_t number_of_global_variable_caches,
size_t number_of_environment_variable_caches,
size_t number_of_registers,
Vector<NonnullOwnPtr<BasicBlock>>,
bool is_strict_mode);
virtual ~Executable() override;
DeprecatedFlyString name;
Vector<u8> bytecode;
Vector<PropertyLookupCache> property_lookup_caches;
Vector<GlobalVariableCache> global_variable_caches;
Vector<EnvironmentVariableCache> environment_variable_caches;
Vector<NonnullOwnPtr<BasicBlock>> basic_blocks;
NonnullOwnPtr<StringTable> string_table;
NonnullOwnPtr<IdentifierTable> identifier_table;
NonnullOwnPtr<RegexTable> regex_table;
@ -73,6 +74,16 @@ public:
size_t number_of_registers { 0 };
bool is_strict_mode { false };
struct ExceptionHandlers {
size_t start_offset;
size_t end_offset;
Optional<size_t> handler_offset;
Optional<size_t> finalizer_offset;
};
Vector<ExceptionHandlers> exception_handlers;
Vector<size_t> basic_block_start_offsets;
ByteString const& get_string(StringTableIndex index) const { return string_table->get(index); }
DeprecatedFlyString const& get_identifier(IdentifierTableIndex index) const { return identifier_table->get(index); }
@ -83,6 +94,10 @@ public:
return get_identifier(*index);
}
[[nodiscard]] Optional<ExceptionHandlers const&> exception_handlers_for_offset(size_t offset) const;
[[nodiscard]] UnrealizedSourceRange source_range_at(size_t offset) const;
void dump() const;
private:

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/QuickSort.h>
#include <AK/TemporaryChange.h>
#include <LibJS/AST.h>
#include <LibJS/Bytecode/BasicBlock.h>
@ -73,7 +74,66 @@ CodeGenerationErrorOr<NonnullGCPtr<Executable>> Generator::generate(VM& vm, ASTN
else if (is<FunctionExpression>(node))
is_strict_mode = static_cast<FunctionExpression const&>(node).is_strict_mode();
size_t size_needed = 0;
for (auto& block : generator.m_root_basic_blocks) {
size_needed += block->size();
}
Vector<u8> bytecode;
bytecode.ensure_capacity(size_needed);
Vector<size_t> basic_block_start_offsets;
basic_block_start_offsets.ensure_capacity(generator.m_root_basic_blocks.size());
HashMap<BasicBlock const*, size_t> block_offsets;
Vector<size_t> label_offsets;
struct UnlinkedExceptionHandlers {
size_t start_offset;
size_t end_offset;
BasicBlock const* handler;
BasicBlock const* finalizer;
};
Vector<UnlinkedExceptionHandlers> unlinked_exception_handlers;
for (auto& block : generator.m_root_basic_blocks) {
basic_block_start_offsets.append(bytecode.size());
if (block->handler() || block->finalizer()) {
unlinked_exception_handlers.append({
.start_offset = bytecode.size(),
.end_offset = 0,
.handler = block->handler(),
.finalizer = block->finalizer(),
});
}
block_offsets.set(block.ptr(), bytecode.size());
Bytecode::InstructionStreamIterator it(block->instruction_stream());
while (!it.at_end()) {
auto& instruction = const_cast<Instruction&>(*it);
instruction.visit_labels([&](Label& label) {
size_t label_offset = bytecode.size() + (bit_cast<FlatPtr>(&label) - bit_cast<FlatPtr>(&instruction));
label_offsets.append(label_offset);
});
bytecode.append(reinterpret_cast<u8 const*>(&instruction), instruction.length());
++it;
}
if (!block->is_terminated()) {
Op::End end(generator.add_constant(js_undefined()));
bytecode.append(reinterpret_cast<u8 const*>(&end), end.length());
}
if (block->handler() || block->finalizer()) {
unlinked_exception_handlers.last().end_offset = bytecode.size();
}
}
for (auto label_offset : label_offsets) {
auto& label = *reinterpret_cast<Label*>(bytecode.data() + label_offset);
auto* block = &label.block();
label.set_address(block_offsets.get(block).value());
}
auto executable = vm.heap().allocate_without_realm<Executable>(
move(bytecode),
move(generator.m_identifier_table),
move(generator.m_string_table),
move(generator.m_regex_table),
@ -83,9 +143,25 @@ CodeGenerationErrorOr<NonnullGCPtr<Executable>> Generator::generate(VM& vm, ASTN
generator.m_next_global_variable_cache,
generator.m_next_environment_variable_cache,
generator.m_next_register,
move(generator.m_root_basic_blocks),
is_strict_mode);
Vector<Executable::ExceptionHandlers> linked_exception_handlers;
for (auto& unlinked_handler : unlinked_exception_handlers) {
auto start_offset = unlinked_handler.start_offset;
auto end_offset = unlinked_handler.end_offset;
auto handler_offset = unlinked_handler.handler ? block_offsets.get(unlinked_handler.handler).value() : Optional<size_t> {};
auto finalizer_offset = unlinked_handler.finalizer ? block_offsets.get(unlinked_handler.finalizer).value() : Optional<size_t> {};
linked_exception_handlers.append({ start_offset, end_offset, handler_offset, finalizer_offset });
}
quick_sort(linked_exception_handlers, [](auto const& a, auto const& b) {
return a.start_offset < b.start_offset;
});
executable->exception_handlers = move(linked_exception_handlers);
executable->basic_block_start_offsets = move(basic_block_start_offsets);
return executable;
}

View file

@ -26,6 +26,22 @@ void Instruction::destroy(Instruction& instruction)
#undef __BYTECODE_OP
}
void Instruction::visit_labels(Function<void(JS::Bytecode::Label&)> visitor)
{
#define __BYTECODE_OP(op) \
case Type::op: \
static_cast<Op::op&>(*this).visit_labels_impl(move(visitor)); \
return;
switch (type()) {
ENUMERATE_BYTECODE_OPS(__BYTECODE_OP)
default:
VERIFY_NOT_REACHED();
}
#undef __BYTECODE_OP
}
UnrealizedSourceRange InstructionStreamIterator::source_range() const
{
VERIFY(m_executable);

View file

@ -137,6 +137,7 @@ public:
size_t length() const { return m_length; }
ByteString to_byte_string(Bytecode::Executable const&) const;
ThrowCompletionOr<void> execute(Bytecode::Interpreter&) const;
void visit_labels(Function<void(Label&)> visitor);
static void destroy(Instruction&);
// FIXME: Find a better way to organize this information
@ -150,6 +151,8 @@ protected:
{
}
void visit_labels_impl(Function<void(Label&)>) { }
private:
SourceRecord m_source_record {};
Type m_type {};

View file

@ -157,7 +157,7 @@ ALWAYS_INLINE Value Interpreter::get(Operand op) const
case Operand::Type::Register:
return reg(Register { op.index() });
case Operand::Type::Local:
return vm().running_execution_context().locals[op.index()];
return m_locals.data()[op.index()];
case Operand::Type::Constant:
return current_executable().constants[op.index()];
}
@ -171,7 +171,7 @@ ALWAYS_INLINE void Interpreter::set(Operand op, Value value)
reg(Register { op.index() }) = value;
return;
case Operand::Type::Local:
vm().running_execution_context().locals[op.index()] = value;
m_locals.data()[op.index()] = value;
return;
case Operand::Type::Constant:
VERIFY_NOT_REACHED();
@ -244,7 +244,7 @@ ThrowCompletionOr<Value> Interpreter::run(Script& script_record, JS::GCPtr<Envir
executable->dump();
// a. Set result to the result of evaluating script.
auto result_or_error = run_executable(*executable, nullptr);
auto result_or_error = run_executable(*executable, {}, {});
if (result_or_error.value.is_error())
result = result_or_error.value.release_error();
else
@ -304,79 +304,113 @@ ThrowCompletionOr<Value> Interpreter::run(SourceTextModule& module)
return js_undefined();
}
void Interpreter::run_bytecode()
Interpreter::HandleExceptionResponse Interpreter::handle_exception(size_t& program_counter, Value exception)
{
auto* locals = vm().running_execution_context().locals.data();
reg(Register::exception()) = exception;
m_scheduled_jump = {};
auto handlers = current_executable().exception_handlers_for_offset(program_counter);
if (!handlers.has_value()) {
return HandleExceptionResponse::ExitFromExecutable;
}
auto& handler = handlers->handler_offset;
auto& finalizer = handlers->finalizer_offset;
VERIFY(!vm().running_execution_context().unwind_contexts.is_empty());
auto& unwind_context = vm().running_execution_context().unwind_contexts.last();
VERIFY(unwind_context.executable == m_current_executable);
if (handler.has_value()) {
program_counter = handler.value();
return HandleExceptionResponse::ContinueInThisExecutable;
}
if (finalizer.has_value()) {
program_counter = finalizer.value();
return HandleExceptionResponse::ContinueInThisExecutable;
}
VERIFY_NOT_REACHED();
}
#define NEXT_INSTRUCTION() \
program_counter += instruction.length(); \
goto start;
void Interpreter::run_bytecode(size_t entry_point)
{
auto& running_execution_context = vm().running_execution_context();
auto* locals = running_execution_context.locals.data();
auto& accumulator = this->accumulator();
auto& executable = current_executable();
auto const* bytecode = executable.bytecode.data();
size_t program_counter = entry_point;
TemporaryChange change(m_program_counter, Optional<size_t&>(program_counter));
for (;;) {
start:
auto pc = InstructionStreamIterator { m_current_block->instruction_stream(), m_current_executable };
TemporaryChange temp_change { m_pc, Optional<InstructionStreamIterator&>(pc) };
bool will_return = false;
bool will_yield = false;
ThrowCompletionOr<void> result;
while (!pc.at_end()) {
auto& instruction = *pc;
for (;;) {
auto& instruction = *reinterpret_cast<Instruction const*>(&bytecode[program_counter]);
switch (instruction.type()) {
case Instruction::Type::SetLocal:
locals[static_cast<Op::SetLocal const&>(instruction).index()] = get(static_cast<Op::SetLocal const&>(instruction).src());
break;
NEXT_INSTRUCTION();
case Instruction::Type::Mov:
set(static_cast<Op::Mov const&>(instruction).dst(), get(static_cast<Op::Mov const&>(instruction).src()));
break;
NEXT_INSTRUCTION();
case Instruction::Type::End:
accumulator = get(static_cast<Op::End const&>(instruction).value());
return;
case Instruction::Type::Jump:
m_current_block = &static_cast<Op::Jump const&>(instruction).target().block();
program_counter = static_cast<Op::Jump const&>(instruction).target().address();
goto start;
case Instruction::Type::JumpIf: {
auto& jump = static_cast<Op::JumpIf const&>(instruction);
if (get(jump.condition()).to_boolean())
m_current_block = &jump.true_target().block();
program_counter = jump.true_target().address();
else
m_current_block = &jump.false_target().block();
program_counter = jump.false_target().address();
goto start;
}
case Instruction::Type::JumpNullish: {
auto& jump = static_cast<Op::JumpNullish const&>(instruction);
if (get(jump.condition()).is_nullish())
m_current_block = &jump.true_target().block();
program_counter = jump.true_target().address();
else
m_current_block = &jump.false_target().block();
program_counter = jump.false_target().address();
goto start;
}
case Instruction::Type::JumpUndefined: {
auto& jump = static_cast<Op::JumpUndefined const&>(instruction);
if (get(jump.condition()).is_undefined())
m_current_block = &jump.true_target().block();
program_counter = jump.true_target().address();
else
m_current_block = &jump.false_target().block();
program_counter = jump.false_target().address();
goto start;
}
case Instruction::Type::EnterUnwindContext:
enter_unwind_context();
m_current_block = &static_cast<Op::EnterUnwindContext const&>(instruction).entry_point().block();
program_counter = static_cast<Op::EnterUnwindContext const&>(instruction).entry_point().address();
goto start;
case Instruction::Type::ContinuePendingUnwind: {
if (auto exception = reg(Register::exception()); !exception.is_empty()) {
result = throw_completion(exception);
break;
if (handle_exception(program_counter, exception) == HandleExceptionResponse::ExitFromExecutable)
return;
goto start;
}
if (!saved_return_value().is_empty()) {
do_return(saved_return_value());
break;
goto may_return;
}
auto& running_execution_context = vm().running_execution_context();
auto const* old_scheduled_jump = running_execution_context.previously_scheduled_jumps.take_last();
if (m_scheduled_jump) {
m_current_block = exchange(m_scheduled_jump, nullptr);
auto const old_scheduled_jump = running_execution_context.previously_scheduled_jumps.take_last();
if (m_scheduled_jump.has_value()) {
program_counter = m_scheduled_jump.value();
m_scheduled_jump = {};
} else {
m_current_block = &static_cast<Op::ContinuePendingUnwind const&>(instruction).resume_target().block();
program_counter = static_cast<Op::ContinuePendingUnwind const&>(instruction).resume_target().address();
// set the scheduled jump to the old value if we continue
// where we left it
m_scheduled_jump = old_scheduled_jump;
@ -384,42 +418,27 @@ void Interpreter::run_bytecode()
goto start;
}
case Instruction::Type::ScheduleJump: {
m_scheduled_jump = &static_cast<Op::ScheduleJump const&>(instruction).target().block();
auto const* finalizer = m_current_block->finalizer();
VERIFY(finalizer);
m_current_block = finalizer;
m_scheduled_jump = static_cast<Op::ScheduleJump const&>(instruction).target().address();
auto finalizer = executable.exception_handlers_for_offset(program_counter).value().finalizer_offset;
VERIFY(finalizer.has_value());
program_counter = finalizer.value();
goto start;
}
default:
result = instruction.execute(*this);
break;
}
if (result.is_error()) [[unlikely]] {
reg(Register::exception()) = *result.throw_completion().value();
m_scheduled_jump = {};
auto const* handler = m_current_block->handler();
auto const* finalizer = m_current_block->finalizer();
if (!handler && !finalizer)
{
auto result = instruction.execute(*this);
if (result.is_error()) {
if (handle_exception(program_counter, *result.throw_completion().value()) == HandleExceptionResponse::ExitFromExecutable)
return;
auto& running_execution_context = vm().running_execution_context();
auto& unwind_context = running_execution_context.unwind_contexts.last();
VERIFY(unwind_context.executable == m_current_executable);
if (handler) {
m_current_block = handler;
goto start;
}
if (finalizer) {
m_current_block = finalizer;
goto start;
}
// An unwind context with no handler or finalizer? We have nowhere to jump, and continuing on will make us crash on the next `Call` to a non-native function if there's an exception! So let's crash here instead.
// If you run into this, you probably forgot to remove the current unwind_context somewhere.
VERIFY_NOT_REACHED();
}
may_return:
if (!reg(Register::return_value()).is_empty()) {
will_return = true;
// Note: A `yield` statement will not go through a finally statement,
@ -430,51 +449,55 @@ void Interpreter::run_bytecode()
will_yield = (instruction.type() == Instruction::Type::Yield && static_cast<Op::Yield const&>(instruction).continuation().has_value()) || instruction.type() == Instruction::Type::Await;
break;
}
++pc;
NEXT_INSTRUCTION();
}
if (auto const* finalizer = m_current_block->finalizer(); finalizer && !will_yield) {
auto& running_execution_context = vm().running_execution_context();
if (!will_yield) {
if (auto handlers = executable.exception_handlers_for_offset(program_counter); handlers.has_value()) {
if (auto finalizer = handlers.value().finalizer_offset; finalizer.has_value()) {
VERIFY(!running_execution_context.unwind_contexts.is_empty());
auto& unwind_context = running_execution_context.unwind_contexts.last();
VERIFY(unwind_context.executable == m_current_executable);
reg(Register::saved_return_value()) = reg(Register::return_value());
reg(Register::return_value()) = {};
m_current_block = finalizer;
program_counter = finalizer.value();
// the unwind_context will be pop'ed when entering the finally block
continue;
}
if (pc.at_end())
break;
}
}
if (will_return)
break;
}
}
Interpreter::ResultAndReturnRegister Interpreter::run_executable(Executable& executable, BasicBlock const* entry_point)
Interpreter::ResultAndReturnRegister Interpreter::run_executable(Executable& executable, Optional<size_t> entry_point, Value initial_accumulator_value)
{
dbgln_if(JS_BYTECODE_DEBUG, "Bytecode::Interpreter will run unit {:p}", &executable);
TemporaryChange restore_executable { m_current_executable, GCPtr { executable } };
TemporaryChange restore_saved_jump { m_scheduled_jump, static_cast<BasicBlock const*>(nullptr) };
TemporaryChange restore_saved_jump { m_scheduled_jump, Optional<size_t> {} };
TemporaryChange restore_realm { m_realm, GCPtr { vm().current_realm() } };
TemporaryChange restore_global_object { m_global_object, GCPtr { m_realm->global_object() } };
TemporaryChange restore_global_declarative_environment { m_global_declarative_environment, GCPtr { m_realm->global_environment().declarative_record() } };
VERIFY(!vm().execution_context_stack().is_empty());
TemporaryChange restore_current_block { m_current_block, entry_point ?: executable.basic_blocks.first() };
auto& running_execution_context = vm().running_execution_context();
if (running_execution_context.registers.size() < executable.number_of_registers)
running_execution_context.registers.resize(executable.number_of_registers);
TemporaryChange restore_registers { m_registers, running_execution_context.registers.span() };
TemporaryChange restore_locals { m_locals, running_execution_context.locals.span() };
reg(Register::accumulator()) = initial_accumulator_value;
reg(Register::return_value()) = {};
running_execution_context.executable = &executable;
run_bytecode();
run_bytecode(entry_point.value_or(0));
dbgln_if(JS_BYTECODE_DEBUG, "Bytecode::Interpreter did run unit {:p}", &executable);
@ -514,7 +537,7 @@ void Interpreter::enter_unwind_context()
m_current_executable,
vm().running_execution_context().lexical_environment);
running_execution_context.previously_scheduled_jumps.append(m_scheduled_jump);
m_scheduled_jump = nullptr;
m_scheduled_jump = {};
}
void Interpreter::leave_unwind_context()
@ -1465,9 +1488,9 @@ ThrowCompletionOr<void> Yield::execute_impl(Bytecode::Interpreter& interpreter)
if (m_continuation_label.has_value())
// FIXME: If we get a pointer, which is not accurately representable as a double
// will cause this to explode
object->define_direct_property("continuation", Value(static_cast<double>(reinterpret_cast<u64>(&m_continuation_label->block()))), JS::default_attributes);
object->define_direct_property("continuation", Value(m_continuation_label->address()), JS::default_attributes);
else
object->define_direct_property("continuation", Value(0), JS::default_attributes);
object->define_direct_property("continuation", js_null(), JS::default_attributes);
object->define_direct_property("isAwait", Value(false), JS::default_attributes);
interpreter.do_return(object);
@ -1482,7 +1505,7 @@ ThrowCompletionOr<void> Await::execute_impl(Bytecode::Interpreter& interpreter)
object->define_direct_property("result", yielded_value, JS::default_attributes);
// FIXME: If we get a pointer, which is not accurately representable as a double
// will cause this to explode
object->define_direct_property("continuation", Value(static_cast<double>(reinterpret_cast<u64>(&m_continuation_label.block()))), JS::default_attributes);
object->define_direct_property("continuation", Value(m_continuation_label.address()), JS::default_attributes);
object->define_direct_property("isAwait", Value(true), JS::default_attributes);
interpreter.do_return(object);
return {};
@ -2074,8 +2097,8 @@ ByteString ContinuePendingUnwind::to_byte_string_impl(Bytecode::Executable const
ByteString Yield::to_byte_string_impl(Bytecode::Executable const& executable) const
{
if (m_continuation_label.has_value()) {
return ByteString::formatted("Yield continuation:@{}, {}",
m_continuation_label->block().name(),
return ByteString::formatted("Yield continuation:{}, {}",
m_continuation_label.value(),
format_operand("value"sv, m_value, executable));
}
return ByteString::formatted("Yield return {}",
@ -2084,9 +2107,9 @@ ByteString Yield::to_byte_string_impl(Bytecode::Executable const& executable) co
ByteString Await::to_byte_string_impl(Bytecode::Executable const& executable) const
{
return ByteString::formatted("Await {}, continuation:@{}",
return ByteString::formatted("Await {}, continuation:{}",
format_operand("argument"sv, m_argument, executable),
m_continuation_label.block().name());
m_continuation_label);
}
ByteString GetByValue::to_byte_string_impl(Bytecode::Executable const& executable) const

View file

@ -33,9 +33,9 @@ public:
ThrowCompletionOr<Value> run(Script&, JS::GCPtr<Environment> lexical_environment_override = nullptr);
ThrowCompletionOr<Value> run(SourceTextModule&);
ThrowCompletionOr<Value> run(Bytecode::Executable& executable, Bytecode::BasicBlock const* entry_point = nullptr)
ThrowCompletionOr<Value> run(Bytecode::Executable& executable, Optional<size_t> entry_point = {}, Value initial_accumulator_value = {})
{
auto result_and_return_register = run_executable(executable, entry_point);
auto result_and_return_register = run_executable(executable, entry_point, initial_accumulator_value);
return move(result_and_return_register.value);
}
@ -43,17 +43,17 @@ public:
ThrowCompletionOr<Value> value;
Value return_register_value;
};
ResultAndReturnRegister run_executable(Bytecode::Executable&, Bytecode::BasicBlock const* entry_point);
ResultAndReturnRegister run_executable(Bytecode::Executable&, Optional<size_t> entry_point, Value initial_accumulator_value = {});
ALWAYS_INLINE Value& accumulator() { return reg(Register::accumulator()); }
ALWAYS_INLINE Value& saved_return_value() { return reg(Register::saved_return_value()); }
Value& reg(Register const& r)
{
return vm().running_execution_context().registers[r.index()];
return m_registers.data()[r.index()];
}
Value reg(Register const& r) const
{
return vm().running_execution_context().registers[r.index()];
return m_registers.data()[r.index()];
}
[[nodiscard]] Value get(Operand) const;
@ -75,23 +75,29 @@ public:
Executable& current_executable() { return *m_current_executable; }
Executable const& current_executable() const { return *m_current_executable; }
BasicBlock const& current_block() const { return *m_current_block; }
Optional<InstructionStreamIterator const&> instruction_stream_iterator() const { return m_pc; }
Optional<size_t> program_counter() const { return m_program_counter; }
Vector<Value>& registers() { return vm().running_execution_context().registers; }
Vector<Value> const& registers() const { return vm().running_execution_context().registers; }
private:
void run_bytecode();
void run_bytecode(size_t entry_point);
enum class HandleExceptionResponse {
ExitFromExecutable,
ContinueInThisExecutable,
};
[[nodiscard]] HandleExceptionResponse handle_exception(size_t& program_counter, Value exception);
VM& m_vm;
BasicBlock const* m_scheduled_jump { nullptr };
Optional<size_t> m_scheduled_jump;
GCPtr<Executable> m_current_executable { nullptr };
BasicBlock const* m_current_block { nullptr };
GCPtr<Realm> m_realm { nullptr };
GCPtr<Object> m_global_object { nullptr };
GCPtr<DeclarativeEnvironment> m_global_declarative_environment { nullptr };
Optional<InstructionStreamIterator&> m_pc {};
Optional<size_t&> m_program_counter;
Span<Value> m_registers;
Span<Value> m_locals;
};
extern bool g_dump_bytecode;

View file

@ -18,18 +18,30 @@ public:
{
}
auto& block() const { return *m_block; }
// Used while compiling.
BasicBlock const& block() const { return *m_block; }
// Used after compiling.
size_t address() const { return m_address; }
void set_address(size_t address) { m_address = address; }
private:
union {
// Relevant while compiling.
BasicBlock const* m_block { nullptr };
// Relevant after compiling.
size_t m_address;
};
};
}
template<>
struct AK::Formatter<JS::Bytecode::Label> : AK::Formatter<FormatString> {
ErrorOr<void> format(FormatBuilder& builder, JS::Bytecode::Label const& value)
ErrorOr<void> format(FormatBuilder& builder, JS::Bytecode::Label const& label)
{
return AK::Formatter<FormatString>::format(builder, "@{}"sv, value.block().name());
return AK::Formatter<FormatString>::format(builder, "@{:x}"sv, label.address());
}
};

View file

@ -1069,6 +1069,10 @@ public:
ThrowCompletionOr<void> execute_impl(Bytecode::Interpreter&) const;
ByteString to_byte_string_impl(Bytecode::Executable const&) const;
void visit_labels_impl(Function<void(Label&)> visitor)
{
visitor(m_target);
}
auto& target() const { return m_target; }
@ -1090,6 +1094,11 @@ public:
ThrowCompletionOr<void> execute_impl(Bytecode::Interpreter&) const;
ByteString to_byte_string_impl(Bytecode::Executable const&) const;
void visit_labels_impl(Function<void(Label&)> visitor)
{
visitor(m_true_target);
visitor(m_false_target);
}
Operand condition() const { return m_condition; }
auto& true_target() const { return m_true_target; }
@ -1115,6 +1124,11 @@ public:
ThrowCompletionOr<void> execute_impl(Bytecode::Interpreter&) const;
ByteString to_byte_string_impl(Bytecode::Executable const&) const;
void visit_labels_impl(Function<void(Label&)> visitor)
{
visitor(m_true_target);
visitor(m_false_target);
}
Operand condition() const { return m_condition; }
auto& true_target() const { return m_true_target; }
@ -1140,6 +1154,11 @@ public:
ThrowCompletionOr<void> execute_impl(Bytecode::Interpreter&) const;
ByteString to_byte_string_impl(Bytecode::Executable const&) const;
void visit_labels_impl(Function<void(Label&)> visitor)
{
visitor(m_true_target);
visitor(m_false_target);
}
Operand condition() const { return m_condition; }
auto& true_target() const { return m_true_target; }
@ -1501,6 +1520,10 @@ public:
ThrowCompletionOr<void> execute_impl(Bytecode::Interpreter&) const;
ByteString to_byte_string_impl(Bytecode::Executable const&) const;
void visit_labels_impl(Function<void(Label&)> visitor)
{
visitor(m_entry_point);
}
auto& entry_point() const { return m_entry_point; }
@ -1531,6 +1554,10 @@ public:
ThrowCompletionOr<void> execute_impl(Bytecode::Interpreter&) const;
ByteString to_byte_string_impl(Bytecode::Executable const&) const;
void visit_labels_impl(Function<void(Label&)> visitor)
{
visitor(m_target);
}
private:
Label m_target;
@ -1570,6 +1597,10 @@ public:
ThrowCompletionOr<void> execute_impl(Bytecode::Interpreter&) const;
ByteString to_byte_string_impl(Bytecode::Executable const&) const;
void visit_labels_impl(Function<void(Label&)> visitor)
{
visitor(m_resume_target);
}
auto& resume_target() const { return m_resume_target; }
@ -1596,6 +1627,11 @@ public:
ThrowCompletionOr<void> execute_impl(Bytecode::Interpreter&) const;
ByteString to_byte_string_impl(Bytecode::Executable const&) const;
void visit_labels_impl(Function<void(Label&)> visitor)
{
if (m_continuation_label.has_value())
visitor(m_continuation_label.value());
}
auto& continuation() const { return m_continuation_label; }
Operand value() const { return m_value; }
@ -1618,6 +1654,10 @@ public:
ThrowCompletionOr<void> execute_impl(Bytecode::Interpreter&) const;
ByteString to_byte_string_impl(Bytecode::Executable const&) const;
void visit_labels_impl(Function<void(Label&)> visitor)
{
visitor(m_continuation_label);
}
auto& continuation() const { return m_continuation_label; }
Operand argument() const { return m_argument; }

View file

@ -693,7 +693,7 @@ ThrowCompletionOr<Value> perform_eval(VM& vm, Value x, CallerMode strict_caller,
executable->name = "eval"sv;
if (Bytecode::g_dump_bytecode)
executable->dump();
auto result_or_error = vm.bytecode_interpreter().run_executable(*executable, nullptr);
auto result_or_error = vm.bytecode_interpreter().run_executable(*executable, {});
if (result_or_error.value.is_error())
return result_or_error.value.release_error();

View file

@ -161,12 +161,14 @@ void AsyncGenerator::execute(VM& vm, Completion completion)
return value.is_empty() ? js_undefined() : value;
};
auto generated_continuation = [&](Value value) -> Bytecode::BasicBlock const* {
auto generated_continuation = [&](Value value) -> Optional<size_t> {
if (value.is_object()) {
auto number_value = value.as_object().get_without_side_effects("continuation");
return reinterpret_cast<Bytecode::BasicBlock const*>(static_cast<u64>(number_value.as_double()));
if (number_value.is_null())
return {};
return static_cast<size_t>(number_value.as_double());
}
return nullptr;
return {};
};
auto generated_is_await = [](Value value) -> bool {
@ -182,16 +184,12 @@ void AsyncGenerator::execute(VM& vm, Completion completion)
auto& bytecode_interpreter = vm.bytecode_interpreter();
auto const* next_block = generated_continuation(m_previous_value);
auto const continuation_address = generated_continuation(m_previous_value);
// We should never enter `execute` again after the generator is complete.
VERIFY(next_block);
VERIFY(continuation_address.has_value());
VERIFY(!m_generating_function->bytecode_executable()->basic_blocks.find_if([next_block](auto& block) { return block == next_block; }).is_end());
bytecode_interpreter.accumulator() = completion_object;
auto next_result = bytecode_interpreter.run_executable(*m_generating_function->bytecode_executable(), next_block);
auto next_result = bytecode_interpreter.run_executable(*m_generating_function->bytecode_executable(), continuation_address, completion_object);
auto result_value = move(next_result.value);
if (!result_value.is_throw_completion()) {
@ -209,7 +207,7 @@ void AsyncGenerator::execute(VM& vm, Completion completion)
}
}
bool done = result_value.is_throw_completion() || generated_continuation(m_previous_value) == nullptr;
bool done = result_value.is_throw_completion() || !generated_continuation(m_previous_value).has_value();
if (!done) {
// 27.6.3.8 AsyncGeneratorYield ( value ), https://tc39.es/ecma262/#sec-asyncgeneratoryield
// 1. Let genContext be the running execution context.

View file

@ -384,7 +384,7 @@ ThrowCompletionOr<Value> ECMAScriptFunctionObject::internal_call(Value this_argu
// Non-standard
callee_context->arguments.append(arguments_list.data(), arguments_list.size());
callee_context->instruction_stream_iterator = vm.bytecode_interpreter().instruction_stream_iterator();
callee_context->program_counter = vm.bytecode_interpreter().program_counter();
// 2. Let calleeContext be PrepareForOrdinaryCall(F, undefined).
// NOTE: We throw if the end of the native stack is reached, so unlike in the spec this _does_ need an exception check.
@ -455,7 +455,7 @@ ThrowCompletionOr<NonnullGCPtr<Object>> ECMAScriptFunctionObject::internal_const
// Non-standard
callee_context->arguments.append(arguments_list.data(), arguments_list.size());
callee_context->instruction_stream_iterator = vm.bytecode_interpreter().instruction_stream_iterator();
callee_context->program_counter = vm.bytecode_interpreter().program_counter();
// 4. Let calleeContext be PrepareForOrdinaryCall(F, newTarget).
// NOTE: We throw if the end of the native stack is reached, so unlike in the spec this _does_ need an exception check.
@ -726,7 +726,7 @@ ThrowCompletionOr<void> ECMAScriptFunctionObject::function_declaration_instantia
for (size_t register_index = 0; register_index < saved_registers.size(); ++register_index)
saved_registers[register_index] = {};
auto result_and_return_register = vm.bytecode_interpreter().run_executable(*m_default_parameter_bytecode_executables[default_parameter_index - 1], nullptr);
auto result_and_return_register = vm.bytecode_interpreter().run_executable(*m_default_parameter_bytecode_executables[default_parameter_index - 1], {});
for (size_t register_index = 0; register_index < saved_registers.size(); ++register_index)
running_execution_context.registers[register_index] = saved_registers[register_index];
@ -1124,7 +1124,7 @@ void async_block_start(VM& vm, T const& async_body, PromiseCapability const& pro
if (maybe_executable.is_error())
result = maybe_executable.release_error();
else
result = vm.bytecode_interpreter().run_executable(*maybe_executable.value(), nullptr).value;
result = vm.bytecode_interpreter().run_executable(*maybe_executable.value(), {}).value;
}
// b. Else,
else {
@ -1251,7 +1251,7 @@ Completion ECMAScriptFunctionObject::ordinary_call_evaluate_body()
}
}
auto result_and_frame = vm.bytecode_interpreter().run_executable(*m_bytecode_executable, nullptr);
auto result_and_frame = vm.bytecode_interpreter().run_executable(*m_bytecode_executable, {});
if (result_and_frame.value.is_error())
return result_and_frame.value.release_error();

View file

@ -36,7 +36,7 @@ NonnullOwnPtr<ExecutionContext> ExecutionContext::copy() const
copy->lexical_environment = lexical_environment;
copy->variable_environment = variable_environment;
copy->private_environment = private_environment;
copy->instruction_stream_iterator = instruction_stream_iterator;
copy->program_counter = program_counter;
copy->function_name = function_name;
copy->this_value = this_value;
copy->is_strict_mode = is_strict_mode;
@ -60,8 +60,6 @@ void ExecutionContext::visit_edges(Cell::Visitor& visitor)
visitor.visit(context_owner);
visitor.visit(this_value);
visitor.visit(executable);
if (instruction_stream_iterator.has_value())
visitor.visit(const_cast<Bytecode::Executable*>(instruction_stream_iterator.value().executable()));
visitor.visit(function_name);
visitor.visit(arguments);
visitor.visit(locals);

View file

@ -11,7 +11,6 @@
#include <AK/DeprecatedFlyString.h>
#include <AK/WeakPtr.h>
#include <LibJS/Bytecode/BasicBlock.h>
#include <LibJS/Bytecode/Instruction.h>
#include <LibJS/Forward.h>
#include <LibJS/Module.h>
#include <LibJS/Runtime/PrivateEnvironment.h>
@ -51,7 +50,7 @@ public:
// Non-standard: This points at something that owns this ExecutionContext, in case it needs to be protected from GC.
GCPtr<Cell> context_owner;
Optional<Bytecode::InstructionStreamIterator> instruction_stream_iterator;
Optional<size_t> program_counter;
GCPtr<PrimitiveString> function_name;
Value this_value;
bool is_strict_mode { false };
@ -78,7 +77,7 @@ public:
Vector<Value> locals;
Vector<Value> registers;
Vector<Bytecode::UnwindInfo> unwind_contexts;
Vector<Bytecode::BasicBlock const*> previously_scheduled_jumps;
Vector<Optional<size_t>> previously_scheduled_jumps;
Vector<GCPtr<Environment>> saved_lexical_environments;
};

View file

@ -88,12 +88,14 @@ ThrowCompletionOr<Value> GeneratorObject::execute(VM& vm, Completion const& comp
return value.is_empty() ? js_undefined() : value;
};
auto generated_continuation = [&](Value value) -> Bytecode::BasicBlock const* {
auto generated_continuation = [&](Value value) -> Optional<size_t> {
if (value.is_object()) {
auto number_value = value.as_object().get_without_side_effects("continuation");
return reinterpret_cast<Bytecode::BasicBlock const*>(static_cast<u64>(number_value.as_double()));
if (number_value.is_null())
return {};
return static_cast<u64>(number_value.as_double());
}
return nullptr;
return {};
};
auto& realm = *vm.current_realm();
@ -103,16 +105,12 @@ ThrowCompletionOr<Value> GeneratorObject::execute(VM& vm, Completion const& comp
auto& bytecode_interpreter = vm.bytecode_interpreter();
auto const* next_block = generated_continuation(m_previous_value);
auto const next_block = generated_continuation(m_previous_value);
// We should never enter `execute` again after the generator is complete.
VERIFY(next_block);
VERIFY(next_block.has_value());
VERIFY(!m_generating_function->bytecode_executable()->basic_blocks.find_if([next_block](auto& block) { return block == next_block; }).is_end());
bytecode_interpreter.registers()[0] = completion_object;
auto next_result = bytecode_interpreter.run_executable(*m_generating_function->bytecode_executable(), next_block);
auto next_result = bytecode_interpreter.run_executable(*m_generating_function->bytecode_executable(), next_block, completion_object);
vm.pop_execution_context();
@ -123,7 +121,7 @@ ThrowCompletionOr<Value> GeneratorObject::execute(VM& vm, Completion const& comp
return result_value;
}
m_previous_value = result_value.release_value();
bool done = generated_continuation(m_previous_value) == nullptr;
bool done = !generated_continuation(m_previous_value).has_value();
m_generator_state = done ? GeneratorState::Completed : GeneratorState::SuspendedYield;
return create_iterator_result_object(vm, generated_value(m_previous_value), done);

View file

@ -147,7 +147,7 @@ ThrowCompletionOr<Value> NativeFunction::internal_call(Value this_argument, Read
// 8. Perform any necessary implementation-defined initialization of calleeContext.
callee_context->this_value = this_argument;
callee_context->arguments.append(arguments_list.data(), arguments_list.size());
callee_context->instruction_stream_iterator = vm.bytecode_interpreter().instruction_stream_iterator();
callee_context->program_counter = vm.bytecode_interpreter().program_counter();
callee_context->lexical_environment = caller_context.lexical_environment;
callee_context->variable_environment = caller_context.variable_environment;
@ -210,7 +210,7 @@ ThrowCompletionOr<NonnullGCPtr<Object>> NativeFunction::internal_construct(Reado
// 8. Perform any necessary implementation-defined initialization of calleeContext.
callee_context->arguments.append(arguments_list.data(), arguments_list.size());
callee_context->instruction_stream_iterator = vm.bytecode_interpreter().instruction_stream_iterator();
callee_context->program_counter = vm.bytecode_interpreter().program_counter();
callee_context->lexical_environment = caller_context.lexical_environment;
callee_context->variable_environment = caller_context.variable_environment;

View file

@ -180,7 +180,7 @@ ThrowCompletionOr<Value> perform_shadow_realm_eval(VM& vm, StringView source_tex
else {
auto executable = maybe_executable.release_value();
auto result_and_return_register = vm.bytecode_interpreter().run_executable(*executable, nullptr);
auto result_and_return_register = vm.bytecode_interpreter().run_executable(*executable, {});
if (result_and_return_register.value.is_error()) {
result = result_and_return_register.value.release_error();
} else {

View file

@ -299,7 +299,7 @@ ThrowCompletionOr<Value> VM::execute_ast_node(ASTNode const& node)
for (size_t i = 0; i < saved_registers.size(); ++i)
running_execution_context.registers[i] = {};
auto result_or_error = bytecode_interpreter().run_executable(*executable, nullptr);
auto result_or_error = bytecode_interpreter().run_executable(*executable, {});
for (size_t i = 0; i < saved_registers.size(); ++i)
running_execution_context.registers[i] = saved_registers[i];
@ -705,8 +705,8 @@ void VM::dump_backtrace() const
{
for (ssize_t i = m_execution_context_stack.size() - 1; i >= 0; --i) {
auto& frame = m_execution_context_stack[i];
if (frame->instruction_stream_iterator.has_value() && frame->instruction_stream_iterator->source_code()) {
auto source_range = frame->instruction_stream_iterator->source_range().realize();
if (frame->program_counter.has_value()) {
auto source_range = frame->executable->source_range_at(frame->program_counter.value()).realize();
dbgln("-> {} @ {}:{},{}", frame->function_name ? frame->function_name->utf8_string() : ""_string, source_range.filename(), source_range.start.line, source_range.start.column);
} else {
dbgln("-> {}", frame->function_name ? frame->function_name->utf8_string() : ""_string);
@ -971,7 +971,7 @@ void VM::load_imported_module(ImportedModuleReferrer referrer, ModuleRequest con
void VM::push_execution_context(ExecutionContext& context)
{
if (!m_execution_context_stack.is_empty())
m_execution_context_stack.last()->instruction_stream_iterator = bytecode_interpreter().instruction_stream_iterator();
m_execution_context_stack.last()->program_counter = bytecode_interpreter().program_counter();
m_execution_context_stack.append(&context);
}
@ -995,10 +995,10 @@ static Optional<UnrealizedSourceRange> get_source_range(ExecutionContext const*
if (!context->executable)
return {};
// Interpreter frame
if (context->instruction_stream_iterator.has_value())
return context->instruction_stream_iterator->source_range();
if (!context->program_counter.has_value())
return {};
return context->executable->source_range_at(context->program_counter.value());
}
Vector<StackTraceElement> VM::stack_trace() const

View file

@ -717,7 +717,7 @@ ThrowCompletionOr<void> SourceTextModule::execute_module(VM& vm, GCPtr<PromiseCa
else {
auto executable = maybe_executable.release_value();
auto result_and_return_register = vm.bytecode_interpreter().run_executable(*executable, nullptr);
auto result_and_return_register = vm.bytecode_interpreter().run_executable(*executable, {});
if (result_and_return_register.value.is_error()) {
result = result_and_return_register.value.release_error();
} else {