
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
429 lines
14 KiB
C++
429 lines
14 KiB
C++
/*
|
|
* Copyright (c) 2021, Andreas Kling <kling@serenityos.org>
|
|
* Copyright (c) 2021, Linus Groh <linusg@serenityos.org>
|
|
* Copyright (c) 2021, Gunnar Beutner <gbeutner@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibJS/AST.h>
|
|
#include <LibJS/Bytecode/Interpreter.h>
|
|
#include <LibJS/Bytecode/Op.h>
|
|
#include <LibJS/Runtime/Array.h>
|
|
#include <LibJS/Runtime/BigInt.h>
|
|
#include <LibJS/Runtime/GlobalObject.h>
|
|
#include <LibJS/Runtime/ScriptFunction.h>
|
|
#include <LibJS/Runtime/Value.h>
|
|
|
|
namespace JS::Bytecode {
|
|
|
|
String Instruction::to_string(Bytecode::Executable const& executable) const
|
|
{
|
|
#define __BYTECODE_OP(op) \
|
|
case Instruction::Type::op: \
|
|
return static_cast<Bytecode::Op::op const&>(*this).to_string(executable);
|
|
|
|
switch (type()) {
|
|
ENUMERATE_BYTECODE_OPS(__BYTECODE_OP)
|
|
default:
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
#undef __BYTECODE_OP
|
|
}
|
|
|
|
}
|
|
|
|
namespace JS::Bytecode::Op {
|
|
|
|
void Load::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.accumulator() = interpreter.reg(m_src);
|
|
}
|
|
|
|
void LoadImmediate::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.accumulator() = m_value;
|
|
}
|
|
|
|
void Store::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.reg(m_dst) = interpreter.accumulator();
|
|
}
|
|
|
|
static Value abstract_inequals(GlobalObject& global_object, Value src1, Value src2)
|
|
{
|
|
return Value(!abstract_eq(global_object, src1, src2));
|
|
}
|
|
|
|
static Value abstract_equals(GlobalObject& global_object, Value src1, Value src2)
|
|
{
|
|
return Value(abstract_eq(global_object, src1, src2));
|
|
}
|
|
|
|
static Value typed_inequals(GlobalObject&, Value src1, Value src2)
|
|
{
|
|
return Value(!strict_eq(src1, src2));
|
|
}
|
|
|
|
static Value typed_equals(GlobalObject&, Value src1, Value src2)
|
|
{
|
|
return Value(strict_eq(src1, src2));
|
|
}
|
|
|
|
#define JS_DEFINE_COMMON_BINARY_OP(OpTitleCase, op_snake_case) \
|
|
void OpTitleCase::execute(Bytecode::Interpreter& interpreter) const \
|
|
{ \
|
|
auto lhs = interpreter.reg(m_lhs_reg); \
|
|
auto rhs = interpreter.accumulator(); \
|
|
interpreter.accumulator() = op_snake_case(interpreter.global_object(), lhs, rhs); \
|
|
} \
|
|
String OpTitleCase::to_string(Bytecode::Executable const&) const \
|
|
{ \
|
|
return String::formatted(#OpTitleCase " {}", m_lhs_reg); \
|
|
}
|
|
|
|
JS_ENUMERATE_COMMON_BINARY_OPS(JS_DEFINE_COMMON_BINARY_OP)
|
|
|
|
static Value not_(GlobalObject&, Value value)
|
|
{
|
|
return Value(!value.to_boolean());
|
|
}
|
|
|
|
static Value typeof_(GlobalObject& global_object, Value value)
|
|
{
|
|
return js_string(global_object.vm(), value.typeof());
|
|
}
|
|
|
|
#define JS_DEFINE_COMMON_UNARY_OP(OpTitleCase, op_snake_case) \
|
|
void OpTitleCase::execute(Bytecode::Interpreter& interpreter) const \
|
|
{ \
|
|
interpreter.accumulator() = op_snake_case(interpreter.global_object(), interpreter.accumulator()); \
|
|
} \
|
|
String OpTitleCase::to_string(Bytecode::Executable const&) const \
|
|
{ \
|
|
return #OpTitleCase; \
|
|
}
|
|
|
|
JS_ENUMERATE_COMMON_UNARY_OPS(JS_DEFINE_COMMON_UNARY_OP)
|
|
|
|
void NewBigInt::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.accumulator() = js_bigint(interpreter.vm().heap(), m_bigint);
|
|
}
|
|
|
|
void NewArray::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
Vector<Value> elements;
|
|
elements.ensure_capacity(m_element_count);
|
|
for (size_t i = 0; i < m_element_count; i++)
|
|
elements.append(interpreter.reg(m_elements[i]));
|
|
interpreter.accumulator() = Array::create_from(interpreter.global_object(), elements);
|
|
}
|
|
|
|
void NewString::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.accumulator() = js_string(interpreter.vm(), interpreter.current_executable().get_string(m_string));
|
|
}
|
|
|
|
void NewObject::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.accumulator() = Object::create_empty(interpreter.global_object());
|
|
}
|
|
|
|
void ConcatString::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.reg(m_lhs) = add(interpreter.global_object(), interpreter.reg(m_lhs), interpreter.accumulator());
|
|
}
|
|
|
|
void GetVariable::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.accumulator() = interpreter.vm().get_variable(interpreter.current_executable().get_string(m_identifier), interpreter.global_object());
|
|
}
|
|
|
|
void SetVariable::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.vm().set_variable(interpreter.current_executable().get_string(m_identifier), interpreter.accumulator(), interpreter.global_object());
|
|
}
|
|
|
|
void GetById::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
if (auto* object = interpreter.accumulator().to_object(interpreter.global_object()))
|
|
interpreter.accumulator() = object->get(interpreter.current_executable().get_string(m_property));
|
|
}
|
|
|
|
void PutById::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
if (auto* object = interpreter.reg(m_base).to_object(interpreter.global_object()))
|
|
object->put(interpreter.current_executable().get_string(m_property), interpreter.accumulator());
|
|
}
|
|
|
|
void Jump::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.jump(*m_true_target);
|
|
}
|
|
|
|
void JumpConditional::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
VERIFY(m_true_target.has_value());
|
|
VERIFY(m_false_target.has_value());
|
|
auto result = interpreter.accumulator();
|
|
if (result.to_boolean())
|
|
interpreter.jump(m_true_target.value());
|
|
else
|
|
interpreter.jump(m_false_target.value());
|
|
}
|
|
|
|
void JumpNullish::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
VERIFY(m_true_target.has_value());
|
|
VERIFY(m_false_target.has_value());
|
|
auto result = interpreter.accumulator();
|
|
if (result.is_nullish())
|
|
interpreter.jump(m_true_target.value());
|
|
else
|
|
interpreter.jump(m_false_target.value());
|
|
}
|
|
|
|
void Call::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
auto callee = interpreter.reg(m_callee);
|
|
if (!callee.is_function()) {
|
|
TODO();
|
|
}
|
|
auto& function = callee.as_function();
|
|
|
|
auto this_value = interpreter.reg(m_this_value);
|
|
|
|
Value return_value;
|
|
|
|
if (m_argument_count == 0) {
|
|
return_value = interpreter.vm().call(function, this_value);
|
|
} else {
|
|
MarkedValueList argument_values { interpreter.vm().heap() };
|
|
for (size_t i = 0; i < m_argument_count; ++i) {
|
|
argument_values.append(interpreter.reg(m_arguments[i]));
|
|
}
|
|
return_value = interpreter.vm().call(function, this_value, move(argument_values));
|
|
}
|
|
|
|
interpreter.accumulator() = return_value;
|
|
}
|
|
|
|
void EnterScope::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
auto& vm = interpreter.vm();
|
|
auto& global_object = interpreter.global_object();
|
|
|
|
for (auto& declaration : m_scope_node.functions())
|
|
vm.current_scope()->put_to_scope(declaration.name(), { js_undefined(), DeclarationKind::Var });
|
|
|
|
for (auto& declaration : m_scope_node.functions()) {
|
|
auto* function = ScriptFunction::create(global_object, declaration.name(), declaration.body(), declaration.parameters(), declaration.function_length(), vm.current_scope(), declaration.is_strict_mode());
|
|
vm.set_variable(declaration.name(), function, global_object);
|
|
}
|
|
|
|
// FIXME: Process variable declarations.
|
|
// FIXME: Whatever else JS::Interpreter::enter_scope() does.
|
|
}
|
|
|
|
void Return::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.do_return(interpreter.accumulator().value_or(js_undefined()));
|
|
}
|
|
|
|
void Increment::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
auto old_value = interpreter.accumulator().to_numeric(interpreter.global_object());
|
|
if (interpreter.vm().exception())
|
|
return;
|
|
|
|
if (old_value.is_number())
|
|
interpreter.accumulator() = Value(old_value.as_double() + 1);
|
|
else
|
|
interpreter.accumulator() = js_bigint(interpreter.vm().heap(), old_value.as_bigint().big_integer().plus(Crypto::SignedBigInteger { 1 }));
|
|
}
|
|
|
|
void Decrement::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
auto old_value = interpreter.accumulator().to_numeric(interpreter.global_object());
|
|
if (interpreter.vm().exception())
|
|
return;
|
|
|
|
if (old_value.is_number())
|
|
interpreter.accumulator() = Value(old_value.as_double() - 1);
|
|
else
|
|
interpreter.accumulator() = js_bigint(interpreter.vm().heap(), old_value.as_bigint().big_integer().minus(Crypto::SignedBigInteger { 1 }));
|
|
}
|
|
|
|
void Throw::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.vm().throw_exception(interpreter.global_object(), interpreter.accumulator());
|
|
}
|
|
|
|
void EnterUnwindContext::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.enter_unwind_context(m_handler_target, m_finalizer_target);
|
|
}
|
|
|
|
void LeaveUnwindContext::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.leave_unwind_context();
|
|
}
|
|
|
|
void ContinuePendingUnwind::execute(Bytecode::Interpreter& interpreter) const
|
|
{
|
|
interpreter.continue_pending_unwind(m_resume_target);
|
|
}
|
|
|
|
String Load::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return String::formatted("Load {}", m_src);
|
|
}
|
|
|
|
String LoadImmediate::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return String::formatted("LoadImmediate {}", m_value);
|
|
}
|
|
|
|
String Store::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return String::formatted("Store {}", m_dst);
|
|
}
|
|
|
|
String NewBigInt::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return String::formatted("NewBigInt \"{}\"", m_bigint.to_base10());
|
|
}
|
|
|
|
String NewArray::to_string(Bytecode::Executable const&) const
|
|
{
|
|
StringBuilder builder;
|
|
builder.append("NewArray");
|
|
if (m_element_count != 0) {
|
|
builder.append(" [");
|
|
for (size_t i = 0; i < m_element_count; ++i) {
|
|
builder.appendff("{}", m_elements[i]);
|
|
if (i != m_element_count - 1)
|
|
builder.append(',');
|
|
}
|
|
builder.append(']');
|
|
}
|
|
return builder.to_string();
|
|
}
|
|
|
|
String NewString::to_string(Bytecode::Executable const& executable) const
|
|
{
|
|
return String::formatted("NewString {} (\"{}\")", m_string, executable.string_table->get(m_string));
|
|
}
|
|
|
|
String NewObject::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return "NewObject";
|
|
}
|
|
|
|
String ConcatString::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return String::formatted("ConcatString {}", m_lhs);
|
|
}
|
|
|
|
String GetVariable::to_string(Bytecode::Executable const& executable) const
|
|
{
|
|
return String::formatted("GetVariable {} ({})", m_identifier, executable.string_table->get(m_identifier));
|
|
}
|
|
|
|
String SetVariable::to_string(Bytecode::Executable const& executable) const
|
|
{
|
|
return String::formatted("SetVariable {} ({})", m_identifier, executable.string_table->get(m_identifier));
|
|
}
|
|
|
|
String PutById::to_string(Bytecode::Executable const& executable) const
|
|
{
|
|
return String::formatted("PutById base:{}, property:{} ({})", m_base, m_property, executable.string_table->get(m_property));
|
|
}
|
|
|
|
String GetById::to_string(Bytecode::Executable const& executable) const
|
|
{
|
|
return String::formatted("GetById {} ({})", m_property, executable.string_table->get(m_property));
|
|
}
|
|
|
|
String Jump::to_string(Bytecode::Executable const&) const
|
|
{
|
|
if (m_true_target.has_value())
|
|
return String::formatted("Jump {}", *m_true_target);
|
|
return String::formatted("Jump <empty>");
|
|
}
|
|
|
|
String JumpConditional::to_string(Bytecode::Executable const&) const
|
|
{
|
|
auto true_string = m_true_target.has_value() ? String::formatted("{}", *m_true_target) : "<empty>";
|
|
auto false_string = m_false_target.has_value() ? String::formatted("{}", *m_false_target) : "<empty>";
|
|
return String::formatted("JumpConditional true:{} false:{}", true_string, false_string);
|
|
}
|
|
|
|
String JumpNullish::to_string(Bytecode::Executable const&) const
|
|
{
|
|
auto true_string = m_true_target.has_value() ? String::formatted("{}", *m_true_target) : "<empty>";
|
|
auto false_string = m_false_target.has_value() ? String::formatted("{}", *m_false_target) : "<empty>";
|
|
return String::formatted("JumpNullish null:{} nonnull:{}", true_string, false_string);
|
|
}
|
|
|
|
String Call::to_string(Bytecode::Executable const&) const
|
|
{
|
|
StringBuilder builder;
|
|
builder.appendff("Call callee:{}, this:{}", m_callee, m_this_value);
|
|
if (m_argument_count != 0) {
|
|
builder.append(", arguments:[");
|
|
for (size_t i = 0; i < m_argument_count; ++i) {
|
|
builder.appendff("{}", m_arguments[i]);
|
|
if (i != m_argument_count - 1)
|
|
builder.append(',');
|
|
}
|
|
builder.append(']');
|
|
}
|
|
return builder.to_string();
|
|
}
|
|
|
|
String EnterScope::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return "EnterScope";
|
|
}
|
|
|
|
String Return::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return "Return";
|
|
}
|
|
|
|
String Increment::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return "Increment";
|
|
}
|
|
|
|
String Decrement::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return "Decrement";
|
|
}
|
|
|
|
String Throw::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return "Throw";
|
|
}
|
|
|
|
String EnterUnwindContext::to_string(Bytecode::Executable const&) const
|
|
{
|
|
auto handler_string = m_handler_target.has_value() ? String::formatted("{}", *m_handler_target) : "<empty>";
|
|
auto finalizer_string = m_finalizer_target.has_value() ? String::formatted("{}", *m_finalizer_target) : "<empty>";
|
|
return String::formatted("EnterUnwindContext handler:{} finalizer:{}", handler_string, finalizer_string);
|
|
}
|
|
|
|
String LeaveUnwindContext::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return "LeaveUnwindContext";
|
|
}
|
|
|
|
String ContinuePendingUnwind::to_string(Bytecode::Executable const&) const
|
|
{
|
|
return String::formatted("ContinuePendingUnwind resume:{}", m_resume_target);
|
|
}
|
|
|
|
}
|