ladybird/Userland/Libraries/LibJS/Bytecode/Generator.h

255 lines
8.8 KiB
C
Raw Normal View History

/*
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullOwnPtrVector.h>
#include <AK/OwnPtr.h>
#include <AK/SinglyLinkedList.h>
#include <LibJS/Bytecode/BasicBlock.h>
#include <LibJS/Bytecode/CodeGenerationError.h>
#include <LibJS/Bytecode/Executable.h>
#include <LibJS/Bytecode/IdentifierTable.h>
#include <LibJS/Bytecode/Label.h>
#include <LibJS/Bytecode/Op.h>
#include <LibJS/Bytecode/Register.h>
#include <LibJS/Bytecode/StringTable.h>
#include <LibJS/Forward.h>
#include <LibJS/Runtime/FunctionKind.h>
namespace JS::Bytecode {
class Generator {
public:
enum class SurroundingScopeKind {
Global,
Function,
Block,
};
static CodeGenerationErrorOr<NonnullOwnPtr<Executable>> generate(ASTNode const&, FunctionKind = FunctionKind::Normal);
Register allocate_register();
void ensure_enough_space(size_t size)
{
// Make sure there's always enough space for a single jump at the end.
if (!m_current_basic_block->can_grow(size + sizeof(Op::Jump))) {
auto& new_block = make_block();
emit<Op::Jump>().set_targets(
Label { new_block },
{});
switch_to_basic_block(new_block);
}
}
template<typename OpType, typename... Args>
OpType& emit(Args&&... args)
{
VERIFY(!is_current_block_terminated());
// If the block doesn't have enough space, switch to another block
if constexpr (!OpType::IsTerminator)
ensure_enough_space(sizeof(OpType));
void* slot = next_slot();
grow(sizeof(OpType));
new (slot) OpType(forward<Args>(args)...);
if constexpr (OpType::IsTerminator)
m_current_basic_block->terminate({}, static_cast<Instruction const*>(slot));
return *static_cast<OpType*>(slot);
}
template<typename OpType, typename... Args>
OpType& emit_with_extra_register_slots(size_t extra_register_slots, Args&&... args)
{
VERIFY(!is_current_block_terminated());
size_t size_to_allocate = round_up_to_power_of_two(sizeof(OpType) + extra_register_slots * sizeof(Register), alignof(void*));
// If the block doesn't have enough space, switch to another block
if constexpr (!OpType::IsTerminator)
ensure_enough_space(size_to_allocate);
void* slot = next_slot();
grow(size_to_allocate);
new (slot) OpType(forward<Args>(args)...);
if constexpr (OpType::IsTerminator)
m_current_basic_block->terminate({}, static_cast<Instruction const*>(slot));
return *static_cast<OpType*>(slot);
}
CodeGenerationErrorOr<void> emit_load_from_reference(JS::ASTNode const&);
CodeGenerationErrorOr<void> emit_store_to_reference(JS::ASTNode const&);
CodeGenerationErrorOr<void> emit_delete_reference(JS::ASTNode const&);
void begin_continuable_scope(Label continue_target, Vector<DeprecatedFlyString> const& language_label_set);
void end_continuable_scope();
void begin_breakable_scope(Label breakable_target, Vector<DeprecatedFlyString> const& language_label_set);
void end_breakable_scope();
[[nodiscard]] Label nearest_continuable_scope() const;
[[nodiscard]] Label nearest_breakable_scope() const;
void switch_to_basic_block(BasicBlock& block)
{
m_current_basic_block = &block;
}
LibJS: Implement bytecode generation for try..catch..finally EnterUnwindContext pushes an unwind context (exception handler and/or finalizer) onto a stack. LeaveUnwindContext pops the unwind context from that stack. Upon return to the interpreter loop we check whether the VM has an exception pending. If no unwind context is available we return from the loop. If an exception handler is available we clear the VM's exception, put the exception value into the accumulator register, clear the unwind context's handler and jump to the handler. If no handler is available but a finalizer is available we save the exception value + metadata (for later use by ContinuePendingUnwind), clear the VM's exception, pop the unwind context and jump to the finalizer. ContinuePendingUnwind checks whether a saved exception is available. If no saved exception is available it jumps to the resume label. Otherwise it stores the exception into the VM. The Jump after LeaveUnwindContext could be integrated into the LeaveUnwindContext instruction. I've kept them separate for now to make the bytecode more readable. > try { 1; throw "x" } catch (e) { 2 } finally { 3 }; 4 1: [ 0] EnterScope [ 10] EnterUnwindContext handler:@4 finalizer:@3 [ 38] EnterScope [ 48] LoadImmediate 1 [ 60] NewString 1 ("x") [ 70] Throw <for non-terminated blocks: insert LeaveUnwindContext + Jump @3 here> 2: [ 0] LoadImmediate 4 3: [ 0] EnterScope [ 10] LoadImmediate 3 [ 28] ContinuePendingUnwind resume:@2 4: [ 0] SetVariable 0 (e) [ 10] EnterScope [ 20] LoadImmediate 2 [ 38] LeaveUnwindContext [ 3c] Jump @3 String Table: 0: e 1: x
2021-06-10 13:04:38 +00:00
[[nodiscard]] BasicBlock& current_block() { return *m_current_basic_block; }
BasicBlock& make_block(DeprecatedString name = {})
{
if (name.is_empty())
name = DeprecatedString::number(m_next_block++);
m_root_basic_blocks.append(BasicBlock::create(name));
return m_root_basic_blocks.last();
}
bool is_current_block_terminated() const
{
return m_current_basic_block->is_terminated();
}
StringTableIndex intern_string(DeprecatedString string)
{
return m_string_table->insert(move(string));
}
IdentifierTableIndex intern_identifier(DeprecatedFlyString string)
{
return m_identifier_table->insert(move(string));
}
bool is_in_generator_or_async_function() const { return m_enclosing_function_kind == FunctionKind::Async || m_enclosing_function_kind == FunctionKind::Generator; }
bool is_in_generator_function() const { return m_enclosing_function_kind == FunctionKind::Generator; }
bool is_in_async_function() const { return m_enclosing_function_kind == FunctionKind::Async; }
enum class BindingMode {
Lexical,
Var,
Global,
};
struct LexicalScope {
SurroundingScopeKind kind;
BindingMode mode;
HashTable<IdentifierTableIndex> known_bindings;
};
void register_binding(IdentifierTableIndex identifier, BindingMode mode = BindingMode::Lexical)
{
m_variable_scopes.last_matching([&](auto& x) { return x.mode == BindingMode::Global || x.mode == mode; })->known_bindings.set(identifier);
}
bool has_binding(IdentifierTableIndex identifier, Optional<BindingMode> const& specific_binding_mode = {}) const
{
for (auto index = m_variable_scopes.size(); index > 0; --index) {
auto& scope = m_variable_scopes[index - 1];
if (scope.mode != BindingMode::Global && specific_binding_mode.value_or(scope.mode) != scope.mode)
continue;
if (scope.known_bindings.contains(identifier))
return true;
}
return false;
}
bool has_binding_in_current_scope(IdentifierTableIndex identifier) const
{
if (m_variable_scopes.is_empty())
return false;
return m_variable_scopes.last().known_bindings.contains(identifier);
}
void begin_variable_scope(BindingMode mode = BindingMode::Lexical, SurroundingScopeKind kind = SurroundingScopeKind::Block);
void end_variable_scope();
enum class BlockBoundaryType {
Break,
Continue,
Unwind,
ReturnToFinally,
LeaveLexicalEnvironment,
LeaveVariableEnvironment,
};
template<typename OpType>
2022-10-16 22:06:11 +00:00
void perform_needed_unwinds(bool is_break_node = false)
requires(OpType::IsTerminator)
{
Optional<BlockBoundaryType> boundary_to_stop_at;
if constexpr (IsSame<OpType, Bytecode::Op::Return> || IsSame<OpType, Bytecode::Op::Yield>)
VERIFY(!is_break_node);
else if constexpr (IsSame<OpType, Bytecode::Op::Throw>)
boundary_to_stop_at = BlockBoundaryType::Unwind;
else
boundary_to_stop_at = is_break_node ? BlockBoundaryType::Break : BlockBoundaryType::Continue;
for (size_t i = m_boundaries.size(); i > 0; --i) {
auto boundary = m_boundaries[i - 1];
if (boundary_to_stop_at.has_value() && boundary == *boundary_to_stop_at)
break;
using enum BlockBoundaryType;
switch (boundary) {
case Unwind:
emit<Bytecode::Op::LeaveUnwindContext>();
break;
case LeaveLexicalEnvironment:
emit<Bytecode::Op::LeaveEnvironment>(Bytecode::Op::EnvironmentMode::Lexical);
break;
case LeaveVariableEnvironment:
emit<Bytecode::Op::LeaveEnvironment>(Bytecode::Op::EnvironmentMode::Var);
break;
case Break:
case Continue:
break;
case ReturnToFinally:
// FIXME: In the case of breaks/continues we need to tell the `finally` to break/continue
// For now let's ignore the finally to avoid a crash
if (IsSame<OpType, Bytecode::Op::Jump>)
break;
return;
};
}
}
void generate_break();
void generate_break(DeprecatedFlyString const& break_label);
Label perform_needed_unwinds_for_labelled_continue_and_return_target_block(DeprecatedFlyString const& continue_label);
void start_boundary(BlockBoundaryType type) { m_boundaries.append(type); }
void end_boundary(BlockBoundaryType type)
{
VERIFY(m_boundaries.last() == type);
m_boundaries.take_last();
}
private:
Generator();
~Generator() = default;
void grow(size_t);
void* next_slot();
struct LabelableScope {
Label bytecode_target;
Vector<DeprecatedFlyString> language_label_set;
};
BasicBlock* m_current_basic_block { nullptr };
NonnullOwnPtrVector<BasicBlock> m_root_basic_blocks;
NonnullOwnPtr<StringTable> m_string_table;
NonnullOwnPtr<IdentifierTable> m_identifier_table;
u32 m_next_register { 2 };
u32 m_next_block { 1 };
FunctionKind m_enclosing_function_kind { FunctionKind::Normal };
Vector<LabelableScope> m_continuable_scopes;
Vector<LabelableScope> m_breakable_scopes;
Vector<LexicalScope> m_variable_scopes;
Vector<BlockBoundaryType> m_boundaries;
};
}