LibWasm: Start implementing a naive bytecode interpreter

As the parser now flattens out the instructions and inserts synthetic
nesting/structured instructions where needed, we can treat the whole
thing as a simple parsed bytecode stream.
This currently knows how to execute the following instructions:
- unreachable
- nop
- local.get
- local.set
- {i,f}{32,64}.const
- block
- loop
- if/else
- branch / branch_if
- i32_add
- i32_and/or/xor
- i32_ne

This also extends the 'wasm' utility to optionally execute the first
function in the module with optionally user-supplied arguments.
This commit is contained in:
Ali Mohammad Pur 2021-05-01 03:19:01 +04:30 committed by Andreas Kling
parent faa34a0a8b
commit 056be42c0b
Notes: sideshowbarker 2024-07-18 17:54:36 +09:00
10 changed files with 513 additions and 30 deletions

View file

@ -422,3 +422,6 @@
#cmakedefine01 WSSCREEN_DEBUG
#endif
#ifndef WASM_TRACE_DEBUG
#cmakedefine01 WASM_TRACE_DEBUG
#endif

View file

@ -179,6 +179,7 @@ set(LINE_EDITOR_DEBUG ON)
set(LANGUAGE_SERVER_DEBUG ON)
set(GL_DEBUG ON)
set(WASM_BINPARSER_DEBUG ON)
set(WASM_TRACE_DEBUG ON)
set(PDF_DEBUG ON)
set(SOLITAIRE_DEBUG ON)

View file

@ -49,7 +49,7 @@ public:
}
template<typename T>
requires(sizeof(T) <= sizeof(u64)) explicit Value(ValueType type, T raw_value)
requires(sizeof(T) == sizeof(u64)) explicit Value(ValueType type, T raw_value)
: m_value(0)
, m_type(type)
{
@ -89,6 +89,33 @@ public:
{
}
Value& operator=(Value&& value)
{
m_value = move(value.m_value);
m_type = move(value.m_type);
return *this;
}
template<typename T>
Optional<T> to()
{
Optional<T> result;
m_value.visit(
[&](auto value) {
if constexpr (!IsSame<T, FunctionAddress> && !IsSame<T, ExternAddress>)
result = value;
},
[&](const FunctionAddress& address) {
if constexpr (IsSame<T, FunctionAddress>)
result = address;
},
[&](const ExternAddress& address) {
if constexpr (IsSame<T, ExternAddress>)
result = address;
});
return result;
}
auto& type() const { return m_type; }
auto& value() const { return m_value; }
@ -317,14 +344,17 @@ private:
class Label {
public:
explicit Label(InstructionPointer continuation)
: m_continuation(continuation)
explicit Label(size_t arity, InstructionPointer continuation)
: m_arity(arity)
, m_continuation(continuation)
{
}
auto continuation() const { return m_continuation; }
auto arity() const { return m_arity; }
private:
size_t m_arity { 0 };
InstructionPointer m_continuation;
};
@ -342,6 +372,7 @@ public:
auto& module() const { return m_module; }
auto& locals() const { return m_locals; }
auto& locals() { return m_locals; }
auto& expression() const { return m_expression; }
auto arity() const { return m_arity; }

View file

@ -74,4 +74,33 @@ Result Configuration::execute()
return Result { move(results_moved) };
}
void Configuration::dump_stack()
{
for (const auto& entry : stack().entries()) {
entry.visit(
[](const NonnullOwnPtr<Value>& v) {
v->value().visit([]<typename T>(const T& v) {
if constexpr (IsIntegral<T> || IsFloatingPoint<T>)
dbgln(" {}", v);
else
dbgln(" *{}", v.value());
});
},
[](const NonnullOwnPtr<Frame>& f) {
dbgln(" frame({})", f->arity());
for (auto& local : f->locals()) {
local.value().visit([]<typename T>(const T& v) {
if constexpr (IsIntegral<T> || IsFloatingPoint<T>)
dbgln(" {}", v);
else
dbgln(" *{}", v.value());
});
}
},
[](const NonnullOwnPtr<Label>& l) {
dbgln(" label({}) -> {}", l->arity(), l->continuation());
});
}
}
}

View file

@ -24,7 +24,7 @@ public:
{
m_current_frame = frame.ptr();
m_stack.push(move(frame));
m_stack.push(make<Label>(m_current_frame->expression().instructions().size() - 1));
m_stack.push(make<Label>(m_current_frame->arity(), m_current_frame->expression().instructions().size() - 1));
}
auto& frame() const { return m_current_frame; }
auto& frame() { return m_current_frame; }
@ -34,10 +34,14 @@ public:
auto& depth() { return m_depth; }
auto& stack() const { return m_stack; }
auto& stack() { return m_stack; }
auto& store() const { return m_store; }
auto& store() { return m_store; }
Result call(FunctionAddress, Vector<Value> arguments);
Result execute();
void dump_stack();
private:
Store& m_store;
Frame* m_current_frame { nullptr };

View file

@ -4,20 +4,403 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <LibWasm/AbstractMachine/AbstractMachine.h>
#include <LibWasm/AbstractMachine/Configuration.h>
#include <LibWasm/AbstractMachine/Interpreter.h>
#include <LibWasm/Opcode.h>
#include <LibWasm/Printer/Printer.h>
namespace Wasm {
void Interpreter::interpret(Configuration& configuration)
{
// FIXME: Interpret stuff
dbgln("FIXME: Interpret stuff!");
// Push some dummy values
for (size_t i = 0; i < configuration.frame()->arity(); ++i)
configuration.stack().push(make<Value>(0));
auto& instructions = configuration.frame()->expression().instructions();
auto max_ip_value = InstructionPointer { instructions.size() };
auto& current_ip_value = configuration.ip();
while (current_ip_value < max_ip_value) {
auto& instruction = instructions[current_ip_value.value()];
interpret(configuration, current_ip_value, instruction);
++current_ip_value;
}
}
void Interpreter::branch_to_label(Configuration& configuration, LabelIndex index)
{
auto label = configuration.nth_label(index.value());
VERIFY(label.has_value());
NonnullOwnPtrVector<Value> results;
// Pop results in order
for (size_t i = 0; i < label->arity(); ++i)
results.append(move(configuration.stack().pop().get<NonnullOwnPtr<Value>>()));
size_t drop_count = index.value() + 1;
if (label->continuation() < configuration.ip())
--drop_count;
for (; !configuration.stack().is_empty();) {
auto entry = configuration.stack().pop();
if (entry.has<NonnullOwnPtr<Label>>()) {
if (drop_count-- == 0)
break;
}
}
// Push results in reverse
for (size_t i = results.size(); i > 0; --i)
configuration.stack().push(move(static_cast<Vector<NonnullOwnPtr<Value>>&>(results)[i - 1]));
configuration.ip() = label->continuation() + 1;
}
ReadonlyBytes Interpreter::load_from_memory(Configuration& configuration, const Instruction& instruction, size_t size)
{
auto& address = configuration.frame()->module().memories().first();
auto memory = configuration.store().get(address);
VERIFY(memory);
auto& arg = instruction.arguments().get<Instruction::MemoryArgument>();
auto base = configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>();
VERIFY(base.has_value());
auto instance_address = base.value() + static_cast<i64>(arg.offset);
if (instance_address < 0 || static_cast<u64>(instance_address + size) > memory->size()) {
dbgln("LibWasm: Memory access out of bounds (expected 0 > {} and {} > {})", instance_address, instance_address + size, memory->size());
return {};
}
return memory->data().bytes().slice(instance_address, size);
}
void Interpreter::interpret(Configuration& configuration, InstructionPointer& ip, const Instruction& instruction)
{
dbgln_if(WASM_TRACE_DEBUG, "Executing instruction {} at ip {}", instruction_name(instruction.opcode()), ip.value());
if constexpr (WASM_TRACE_DEBUG)
configuration.dump_stack();
switch (instruction.opcode().value()) {
case Instructions::unreachable.value():
VERIFY_NOT_REACHED(); // FIXME: This is definitely not right :)
case Instructions::nop.value():
return;
case Instructions::local_get.value():
configuration.stack().push(make<Value>(configuration.frame()->locals()[instruction.arguments().get<LocalIndex>().value()]));
return;
case Instructions::local_set.value(): {
auto entry = configuration.stack().pop();
configuration.frame()->locals()[instruction.arguments().get<LocalIndex>().value()] = move(*entry.get<NonnullOwnPtr<Value>>());
return;
}
case Instructions::i32_const.value():
configuration.stack().push(make<Value>(ValueType { ValueType::I32 }, static_cast<i64>(instruction.arguments().get<i32>())));
return;
case Instructions::i64_const.value():
configuration.stack().push(make<Value>(ValueType { ValueType::I64 }, instruction.arguments().get<i64>()));
return;
case Instructions::f32_const.value():
configuration.stack().push(make<Value>(ValueType { ValueType::F32 }, static_cast<double>(instruction.arguments().get<float>())));
return;
case Instructions::f64_const.value():
configuration.stack().push(make<Value>(ValueType { ValueType::F64 }, instruction.arguments().get<double>()));
return;
case Instructions::block.value(): {
size_t arity = 0;
auto& args = instruction.arguments().get<Instruction::StructuredInstructionArgs>();
if (args.block_type.kind() != BlockType::Empty)
arity = 1;
configuration.stack().push(make<Label>(arity, args.end_ip));
return;
}
case Instructions::loop.value(): {
size_t arity = 0;
auto& args = instruction.arguments().get<Instruction::StructuredInstructionArgs>();
if (args.block_type.kind() != BlockType::Empty)
arity = 1;
configuration.stack().push(make<Label>(arity, ip.value() + 1));
return;
}
case Instructions::if_.value(): {
size_t arity = 0;
auto& args = instruction.arguments().get<Instruction::StructuredInstructionArgs>();
if (args.block_type.kind() != BlockType::Empty)
arity = 1;
auto entry = configuration.stack().pop();
auto value = entry.get<NonnullOwnPtr<Value>>()->to<i32>();
VERIFY(value.has_value());
configuration.stack().push(make<Label>(arity, args.end_ip));
if (value.value() == 0) {
if (args.else_ip.has_value()) {
configuration.ip() = args.else_ip.value();
} else {
configuration.ip() = args.end_ip;
configuration.stack().pop();
}
}
return;
}
case Instructions::structured_end.value():
return;
case Instructions::structured_else.value(): {
auto label = configuration.nth_label(0);
VERIFY(label.has_value());
NonnullOwnPtrVector<Value> results;
// Pop results in order
for (size_t i = 0; i < label->arity(); ++i)
results.append(move(configuration.stack().pop().get<NonnullOwnPtr<Value>>()));
// drop all locals
for (; !configuration.stack().is_empty();) {
auto entry = configuration.stack().pop();
if (entry.has<NonnullOwnPtr<Label>>())
break;
}
// Push results in reverse
for (size_t i = 1; i < results.size() + 1; ++i)
configuration.stack().push(move(static_cast<Vector<NonnullOwnPtr<Value>>&>(results)[results.size() - i]));
if (instruction.opcode() == Instructions::structured_end)
return;
// Jump to the end label
configuration.ip() = label->continuation();
return;
}
case Instructions::br.value():
return branch_to_label(configuration, instruction.arguments().get<LabelIndex>());
case Instructions::br_if.value(): {
if (configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>().value_or(0) == 0)
return;
return branch_to_label(configuration, instruction.arguments().get<LabelIndex>());
}
case Instructions::br_table.value():
case Instructions::call.value():
case Instructions::call_indirect.value():
case Instructions::i32_load.value():
case Instructions::i64_load.value():
case Instructions::f32_load.value():
case Instructions::f64_load.value():
case Instructions::i32_load8_s.value():
goto unimplemented;
case Instructions::i32_load8_u.value(): {
auto slice = load_from_memory(configuration, instruction, 1);
VERIFY(slice.size() == 1);
configuration.stack().push(make<Value>(static_cast<i32>(slice[0])));
return;
}
case Instructions::i32_load16_s.value():
case Instructions::i32_load16_u.value():
case Instructions::i64_load8_s.value():
case Instructions::i64_load8_u.value():
case Instructions::i64_load16_s.value():
case Instructions::i64_load16_u.value():
case Instructions::i64_load32_s.value():
case Instructions::i64_load32_u.value():
case Instructions::i32_store.value():
case Instructions::i64_store.value():
case Instructions::f32_store.value():
case Instructions::f64_store.value():
case Instructions::i32_store8.value():
case Instructions::i32_store16.value():
case Instructions::i64_store8.value():
case Instructions::i64_store16.value():
case Instructions::i64_store32.value():
case Instructions::local_tee.value():
case Instructions::global_get.value():
case Instructions::global_set.value():
case Instructions::memory_size.value():
case Instructions::memory_grow.value():
case Instructions::table_get.value():
case Instructions::table_set.value():
case Instructions::select_typed.value():
case Instructions::ref_null.value():
case Instructions::ref_func.value():
case Instructions::ref_is_null.value():
case Instructions::return_.value():
case Instructions::drop.value():
case Instructions::select.value():
case Instructions::i32_eqz.value():
case Instructions::i32_eq.value():
goto unimplemented;
case Instructions::i32_ne.value(): {
auto lhs = configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>();
auto rhs = configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>();
VERIFY(lhs.has_value());
VERIFY(rhs.has_value());
configuration.stack().push(make<Value>(lhs.value() != rhs.value()));
return;
}
case Instructions::i32_lts.value():
case Instructions::i32_ltu.value():
case Instructions::i32_gts.value():
case Instructions::i32_gtu.value():
case Instructions::i32_les.value():
case Instructions::i32_leu.value():
case Instructions::i32_ges.value():
case Instructions::i32_geu.value():
case Instructions::i64_eqz.value():
case Instructions::i64_eq.value():
case Instructions::i64_ne.value():
case Instructions::i64_lts.value():
case Instructions::i64_ltu.value():
case Instructions::i64_gts.value():
case Instructions::i64_gtu.value():
case Instructions::i64_les.value():
case Instructions::i64_leu.value():
case Instructions::i64_ges.value():
case Instructions::i64_geu.value():
case Instructions::f32_eq.value():
case Instructions::f32_ne.value():
case Instructions::f32_lt.value():
case Instructions::f32_gt.value():
case Instructions::f32_le.value():
case Instructions::f32_ge.value():
case Instructions::f64_eq.value():
case Instructions::f64_ne.value():
case Instructions::f64_lt.value():
case Instructions::f64_gt.value():
case Instructions::f64_le.value():
case Instructions::f64_ge.value():
case Instructions::i32_clz.value():
case Instructions::i32_ctz.value():
case Instructions::i32_popcnt.value():
goto unimplemented;
case Instructions::i32_add.value(): {
auto lhs = configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>();
auto rhs = configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>();
VERIFY(lhs.has_value());
VERIFY(rhs.has_value());
configuration.stack().push(make<Value>(lhs.value() + rhs.value()));
return;
}
case Instructions::i32_sub.value():
case Instructions::i32_mul.value():
case Instructions::i32_divs.value():
case Instructions::i32_divu.value():
case Instructions::i32_rems.value():
case Instructions::i32_remu.value():
goto unimplemented;
case Instructions::i32_and.value(): {
auto lhs = configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>();
auto rhs = configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>();
VERIFY(lhs.has_value());
VERIFY(rhs.has_value());
configuration.stack().push(make<Value>(lhs.value() & rhs.value()));
return;
}
case Instructions::i32_or.value(): {
auto lhs = configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>();
auto rhs = configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>();
VERIFY(lhs.has_value());
VERIFY(rhs.has_value());
configuration.stack().push(make<Value>(lhs.value() | rhs.value()));
return;
}
case Instructions::i32_xor.value(): {
auto lhs = configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>();
auto rhs = configuration.stack().pop().get<NonnullOwnPtr<Value>>()->to<i32>();
VERIFY(lhs.has_value());
VERIFY(rhs.has_value());
configuration.stack().push(make<Value>(lhs.value() ^ rhs.value()));
return;
}
case Instructions::i32_shl.value():
case Instructions::i32_shrs.value():
case Instructions::i32_shru.value():
case Instructions::i32_rotl.value():
case Instructions::i32_rotr.value():
case Instructions::i64_clz.value():
case Instructions::i64_ctz.value():
case Instructions::i64_popcnt.value():
case Instructions::i64_add.value():
case Instructions::i64_sub.value():
case Instructions::i64_mul.value():
case Instructions::i64_divs.value():
case Instructions::i64_divu.value():
case Instructions::i64_rems.value():
case Instructions::i64_remu.value():
case Instructions::i64_and.value():
case Instructions::i64_or.value():
case Instructions::i64_xor.value():
case Instructions::i64_shl.value():
case Instructions::i64_shrs.value():
case Instructions::i64_shru.value():
case Instructions::i64_rotl.value():
case Instructions::i64_rotr.value():
case Instructions::f32_abs.value():
case Instructions::f32_neg.value():
case Instructions::f32_ceil.value():
case Instructions::f32_floor.value():
case Instructions::f32_trunc.value():
case Instructions::f32_nearest.value():
case Instructions::f32_sqrt.value():
case Instructions::f32_add.value():
case Instructions::f32_sub.value():
case Instructions::f32_mul.value():
case Instructions::f32_div.value():
case Instructions::f32_min.value():
case Instructions::f32_max.value():
case Instructions::f32_copysign.value():
case Instructions::f64_abs.value():
case Instructions::f64_neg.value():
case Instructions::f64_ceil.value():
case Instructions::f64_floor.value():
case Instructions::f64_trunc.value():
case Instructions::f64_nearest.value():
case Instructions::f64_sqrt.value():
case Instructions::f64_add.value():
case Instructions::f64_sub.value():
case Instructions::f64_mul.value():
case Instructions::f64_div.value():
case Instructions::f64_min.value():
case Instructions::f64_max.value():
case Instructions::f64_copysign.value():
case Instructions::i32_wrap_i64.value():
case Instructions::i32_trunc_sf32.value():
case Instructions::i32_trunc_uf32.value():
case Instructions::i32_trunc_sf64.value():
case Instructions::i32_trunc_uf64.value():
case Instructions::i64_extend_si32.value():
case Instructions::i64_extend_ui32.value():
case Instructions::i64_trunc_sf32.value():
case Instructions::i64_trunc_uf32.value():
case Instructions::i64_trunc_sf64.value():
case Instructions::i64_trunc_uf64.value():
case Instructions::f32_convert_si32.value():
case Instructions::f32_convert_ui32.value():
case Instructions::f32_convert_si64.value():
case Instructions::f32_convert_ui64.value():
case Instructions::f32_demote_f64.value():
case Instructions::f64_convert_si32.value():
case Instructions::f64_convert_ui32.value():
case Instructions::f64_convert_si64.value():
case Instructions::f64_convert_ui64.value():
case Instructions::f64_promote_f32.value():
case Instructions::i32_reinterpret_f32.value():
case Instructions::i64_reinterpret_f64.value():
case Instructions::f32_reinterpret_i32.value():
case Instructions::f64_reinterpret_i64.value():
case Instructions::i32_trunc_sat_f32_s.value():
case Instructions::i32_trunc_sat_f32_u.value():
case Instructions::i32_trunc_sat_f64_s.value():
case Instructions::i32_trunc_sat_f64_u.value():
case Instructions::i64_trunc_sat_f32_s.value():
case Instructions::i64_trunc_sat_f32_u.value():
case Instructions::i64_trunc_sat_f64_s.value():
case Instructions::i64_trunc_sat_f64_u.value():
case Instructions::memory_init.value():
case Instructions::data_drop.value():
case Instructions::memory_copy.value():
case Instructions::memory_fill.value():
case Instructions::table_init.value():
case Instructions::elem_drop.value():
case Instructions::table_copy.value():
case Instructions::table_grow.value():
case Instructions::table_size.value():
case Instructions::table_fill.value():
default:
unimplemented:;
dbgln("Instruction '{}' not implemented", instruction_name(instruction.opcode()));
return;
}
}
}

View file

@ -12,6 +12,11 @@ namespace Wasm {
struct Interpreter {
void interpret(Configuration&);
private:
void interpret(Configuration&, InstructionPointer&, const Instruction&);
void branch_to_label(Configuration&, LabelIndex);
ReadonlyBytes load_from_memory(Configuration&, const Instruction&, size_t);
};
}

View file

@ -14,7 +14,7 @@ struct Names {
static HashMap<OpCode, String> instruction_names;
};
static String instruction_name(const OpCode& opcode)
String instruction_name(const OpCode& opcode)
{
return Names::instruction_names.get(opcode).value_or("<unknown>");
}

View file

@ -10,6 +10,8 @@
namespace Wasm {
String instruction_name(const OpCode& opcode);
struct Printer {
explicit Printer(OutputStream& stream, size_t initial_indent = 0)
: m_stream(stream)

View file

@ -16,16 +16,31 @@ int main(int argc, char* argv[])
const char* filename = nullptr;
bool print = false;
bool attempt_instantiate = false;
bool attemp_execute = false;
String exported_function_to_execute;
Vector<u64> values_to_push;
Core::ArgsParser parser;
parser.add_positional_argument(filename, "File name to parse", "file");
parser.add_option(print, "Print the parsed module", "print", 'p');
parser.add_option(attempt_instantiate, "Attempt to instantiate the module", "instantiate", 'i');
parser.add_option(attemp_execute, "Attempt to execute a function from the module (implies -i)", "execute", 'e');
parser.add_option(exported_function_to_execute, "Attempt to execute the named exported function from the module (implies -i)", "execute", 'e', "name");
parser.add_option(Core::ArgsParser::Option {
.requires_argument = true,
.help_string = "Supply arguments to the function (default=0) (expects u64, casts to required type)",
.long_name = "arg",
.short_name = 0,
.value_name = "u64",
.accept_value = [&](const char* str) -> bool {
if (auto v = StringView { str }.to_uint<u64>(); v.has_value()) {
values_to_push.append(v.value());
return true;
}
return false;
},
});
parser.parse(argc, argv);
if (attemp_execute)
if (!exported_function_to_execute.is_empty())
attempt_instantiate = true;
auto result = Core::File::open(filename, Core::OpenMode::ReadOnly);
@ -80,30 +95,40 @@ int main(int argc, char* argv[])
}
}
if (attemp_execute) {
if (!exported_function_to_execute.is_empty()) {
Optional<Wasm::FunctionAddress> run_address;
Vector<Wasm::Value> values;
// Pick a function that takes no args :P
for (auto& address : machine.module_instance().functions()) {
auto fn = machine.store().get(address);
if (!fn)
continue;
if (auto ptr = fn->get_pointer<Wasm::WasmFunction>()) {
const Wasm::FunctionType& ty = ptr->type();
for (auto& param : ty.parameters()) {
values.append(Wasm::Value { param, 0ull });
}
run_address = address;
break;
for (auto& entry : machine.module_instance().exports()) {
if (entry.name() == exported_function_to_execute) {
if (auto addr = entry.value().get_pointer<Wasm::FunctionAddress>())
run_address = *addr;
}
}
if (!run_address.has_value()) {
warnln("No nullary function, sorry :(");
warnln("No such exported function, sorry :(");
return 1;
}
outln("Executing ");
print_func(*run_address);
outln();
auto instance = machine.store().get(*run_address);
VERIFY(instance);
if (instance->has<Wasm::HostFunction>()) {
warnln("Exported function is a host function, cannot run that yet");
return 1;
}
for (auto& param : instance->get<Wasm::WasmFunction>().type().parameters()) {
if (values_to_push.is_empty())
values.append(Wasm::Value { param, 0ull });
else
values.append(Wasm::Value { param, values_to_push.take_last() });
}
if (print) {
outln("Executing ");
print_func(*run_address);
outln();
}
auto result = machine.invoke(run_address.value(), move(values));
if (!result.values().is_empty())