mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-08-09 17:49:40 +00:00
LibWasm: Make traps hold on to externally-managed data
...instead of specially handling JS::Completion. This makes it possible for LibWeb/LibJS to have full control over how these things are made, stored, and visited (whenever). Fixes an issue where we couldn't roundtrip a JS exception through Wasm.
This commit is contained in:
parent
7b2a427430
commit
51bab5b186
Notes:
github-actions[bot]
2025-04-22 14:45:25 +00:00
Author: https://github.com/alimpfard
Commit: 51bab5b186
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4423
Reviewed-by: https://github.com/ADKaster ✅
11 changed files with 176 additions and 111 deletions
|
@ -239,6 +239,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
||||||
}
|
}
|
||||||
|
|
||||||
BytecodeInterpreter interpreter(m_stack_info);
|
BytecodeInterpreter interpreter(m_stack_info);
|
||||||
|
auto handle = register_scoped(interpreter);
|
||||||
|
|
||||||
for (auto& entry : module.global_section().entries()) {
|
for (auto& entry : module.global_section().entries()) {
|
||||||
Configuration config { m_store };
|
Configuration config { m_store };
|
||||||
|
@ -250,9 +251,9 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
||||||
entry.expression(),
|
entry.expression(),
|
||||||
1,
|
1,
|
||||||
});
|
});
|
||||||
auto result = config.execute(interpreter).assert_wasm_result();
|
auto result = config.execute(interpreter);
|
||||||
if (result.is_trap())
|
if (result.is_trap())
|
||||||
return InstantiationError { ByteString::formatted("Global value construction trapped: {}", result.trap().reason) };
|
return InstantiationError { "Global instantiation trapped", move(result.trap()) };
|
||||||
global_values.append(result.values().first());
|
global_values.append(result.values().first());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -271,9 +272,9 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
||||||
entry,
|
entry,
|
||||||
entry.instructions().size(),
|
entry.instructions().size(),
|
||||||
});
|
});
|
||||||
auto result = config.execute(interpreter).assert_wasm_result();
|
auto result = config.execute(interpreter);
|
||||||
if (result.is_trap())
|
if (result.is_trap())
|
||||||
return InstantiationError { ByteString::formatted("Element construction trapped: {}", result.trap().reason) };
|
return InstantiationError { "Element section initialisation trapped", move(result.trap()) };
|
||||||
|
|
||||||
for (auto& value : result.values()) {
|
for (auto& value : result.values()) {
|
||||||
auto reference = value.to<Reference>();
|
auto reference = value.to<Reference>();
|
||||||
|
@ -306,9 +307,9 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
||||||
active_ptr->expression,
|
active_ptr->expression,
|
||||||
1,
|
1,
|
||||||
});
|
});
|
||||||
auto result = config.execute(interpreter).assert_wasm_result();
|
auto result = config.execute(interpreter);
|
||||||
if (result.is_trap())
|
if (result.is_trap())
|
||||||
return InstantiationError { ByteString::formatted("Element section initialisation trapped: {}", result.trap().reason) };
|
return InstantiationError { "Element section initialisation trapped", move(result.trap()) };
|
||||||
auto d = result.values().first().to<i32>();
|
auto d = result.values().first().to<i32>();
|
||||||
auto table_instance = m_store.get(main_module_instance.tables()[active_ptr->index.value()]);
|
auto table_instance = m_store.get(main_module_instance.tables()[active_ptr->index.value()]);
|
||||||
if (current_index >= main_module_instance.elements().size())
|
if (current_index >= main_module_instance.elements().size())
|
||||||
|
@ -341,9 +342,9 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
||||||
data.offset,
|
data.offset,
|
||||||
1,
|
1,
|
||||||
});
|
});
|
||||||
auto result = config.execute(interpreter).assert_wasm_result();
|
auto result = config.execute(interpreter);
|
||||||
if (result.is_trap())
|
if (result.is_trap())
|
||||||
return InstantiationError { ByteString::formatted("Data section initialisation trapped: {}", result.trap().reason) };
|
return InstantiationError { "Data section initialisation trapped", move(result.trap()) };
|
||||||
size_t offset = result.values().first().to<u64>();
|
size_t offset = result.values().first().to<u64>();
|
||||||
if (main_module_instance.memories().size() <= data.index.value()) {
|
if (main_module_instance.memories().size() <= data.index.value()) {
|
||||||
return InstantiationError {
|
return InstantiationError {
|
||||||
|
@ -391,7 +392,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
||||||
}
|
}
|
||||||
auto result = invoke(functions[index.value()], {});
|
auto result = invoke(functions[index.value()], {});
|
||||||
if (result.is_trap())
|
if (result.is_trap())
|
||||||
return InstantiationError { ByteString::formatted("Start function trapped: {}", result.trap().reason) };
|
return InstantiationError { "Start function trapped", move(result.trap()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
return InstantiationResult { move(main_module_instance_pointer) };
|
return InstantiationResult { move(main_module_instance_pointer) };
|
||||||
|
@ -491,6 +492,7 @@ Optional<InstantiationError> AbstractMachine::allocate_all_final_phase(Module co
|
||||||
Result AbstractMachine::invoke(FunctionAddress address, Vector<Value> arguments)
|
Result AbstractMachine::invoke(FunctionAddress address, Vector<Value> arguments)
|
||||||
{
|
{
|
||||||
BytecodeInterpreter interpreter(m_stack_info);
|
BytecodeInterpreter interpreter(m_stack_info);
|
||||||
|
auto handle = register_scoped(interpreter);
|
||||||
return invoke(interpreter, address, move(arguments));
|
return invoke(interpreter, address, move(arguments));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -575,4 +577,10 @@ void Linker::populate()
|
||||||
m_unresolved_imports.set(m_ordered_imports.last());
|
m_unresolved_imports.set(m_ordered_imports.last());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AbstractMachine::visit_external_resources(HostVisitOps const& host)
|
||||||
|
{
|
||||||
|
for (auto interpreter_ptr : m_active_interpreters)
|
||||||
|
interpreter_ptr->visit_external_resources(host);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/ByteBuffer.h>
|
||||||
#include <AK/Function.h>
|
#include <AK/Function.h>
|
||||||
#include <AK/HashMap.h>
|
#include <AK/HashMap.h>
|
||||||
#include <AK/HashTable.h>
|
#include <AK/HashTable.h>
|
||||||
|
@ -14,18 +15,13 @@
|
||||||
#include <AK/UFixedBigInt.h>
|
#include <AK/UFixedBigInt.h>
|
||||||
#include <LibWasm/Types.h>
|
#include <LibWasm/Types.h>
|
||||||
|
|
||||||
// NOTE: Special case for Wasm::Result.
|
|
||||||
#include <LibJS/Runtime/Completion.h>
|
|
||||||
|
|
||||||
namespace Wasm {
|
namespace Wasm {
|
||||||
|
|
||||||
class Configuration;
|
class Configuration;
|
||||||
class Result;
|
class Result;
|
||||||
struct Interpreter;
|
struct Interpreter;
|
||||||
|
struct Trap;
|
||||||
|
|
||||||
struct InstantiationError {
|
|
||||||
ByteString error { "Unknown error" };
|
|
||||||
};
|
|
||||||
struct LinkError {
|
struct LinkError {
|
||||||
enum OtherErrors {
|
enum OtherErrors {
|
||||||
InvalidImportedModule,
|
InvalidImportedModule,
|
||||||
|
@ -198,37 +194,43 @@ private:
|
||||||
u128 m_value;
|
u128 m_value;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Trap {
|
struct ExternallyManagedTrap {
|
||||||
ByteString reason;
|
Array<u8, 64> data;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T const& unsafe_external_object_as() const
|
||||||
|
{
|
||||||
|
static_assert(sizeof(T) <= sizeof(data), "Object size is too large for ExternallyManagedTrap");
|
||||||
|
return *reinterpret_cast<T const*>(data.data());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// A variant of Result that does not include external reasons for error (JS::Completion, for now).
|
struct Trap {
|
||||||
class PureResult {
|
Variant<ByteString, ExternallyManagedTrap> data;
|
||||||
public:
|
|
||||||
explicit PureResult(Vector<Value> values)
|
ByteString format() const
|
||||||
: m_result(move(values))
|
|
||||||
{
|
{
|
||||||
|
if (auto const* ptr = data.get_pointer<ByteString>())
|
||||||
|
return *ptr;
|
||||||
|
return "<Externally managed Trap Data>";
|
||||||
}
|
}
|
||||||
|
|
||||||
PureResult(Trap trap)
|
template<typename T>
|
||||||
: m_result(move(trap))
|
static Trap from_external_object(T&& object)
|
||||||
{
|
{
|
||||||
|
static_assert(sizeof(T) <= sizeof(ExternallyManagedTrap::data), "Object size is too large for ExternallyManagedTrap");
|
||||||
|
static_assert(IsTriviallyCopyable<T>, "Object must be trivially copyable");
|
||||||
|
static_assert(IsTriviallyDestructible<T>, "Object must be trivially destructible");
|
||||||
|
|
||||||
|
ExternallyManagedTrap externally_managed_trap;
|
||||||
|
new (externally_managed_trap.data.data()) T(forward<T>(object));
|
||||||
|
return Trap { externally_managed_trap };
|
||||||
}
|
}
|
||||||
|
|
||||||
auto is_trap() const { return m_result.has<Trap>(); }
|
static Trap from_string(ByteString string)
|
||||||
auto& values() const { return m_result.get<Vector<Value>>(); }
|
|
||||||
auto& values() { return m_result.get<Vector<Value>>(); }
|
|
||||||
auto& trap() const { return m_result.get<Trap>(); }
|
|
||||||
auto& trap() { return m_result.get<Trap>(); }
|
|
||||||
|
|
||||||
private:
|
|
||||||
friend class Result;
|
|
||||||
explicit PureResult(Variant<Vector<Value>, Trap>&& result)
|
|
||||||
: m_result(move(result))
|
|
||||||
{
|
{
|
||||||
|
return Trap { move(string) };
|
||||||
}
|
}
|
||||||
|
|
||||||
Variant<Vector<Value>, Trap> m_result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Result {
|
class Result {
|
||||||
|
@ -243,34 +245,24 @@ public:
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Result(JS::Completion completion)
|
|
||||||
: m_result(move(completion))
|
|
||||||
{
|
|
||||||
VERIFY(m_result.get<JS::Completion>().is_abrupt());
|
|
||||||
}
|
|
||||||
|
|
||||||
Result(PureResult&& result)
|
|
||||||
: m_result(result.m_result.downcast<decltype(m_result)>())
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
auto is_trap() const { return m_result.has<Trap>(); }
|
auto is_trap() const { return m_result.has<Trap>(); }
|
||||||
auto is_completion() const { return m_result.has<JS::Completion>(); }
|
|
||||||
auto& values() const { return m_result.get<Vector<Value>>(); }
|
auto& values() const { return m_result.get<Vector<Value>>(); }
|
||||||
auto& values() { return m_result.get<Vector<Value>>(); }
|
auto& values() { return m_result.get<Vector<Value>>(); }
|
||||||
auto& trap() const { return m_result.get<Trap>(); }
|
auto& trap() const { return m_result.get<Trap>(); }
|
||||||
auto& trap() { return m_result.get<Trap>(); }
|
auto& trap() { return m_result.get<Trap>(); }
|
||||||
auto& completion() { return m_result.get<JS::Completion>(); }
|
|
||||||
auto& completion() const { return m_result.get<JS::Completion>(); }
|
|
||||||
|
|
||||||
PureResult assert_wasm_result() &&
|
|
||||||
{
|
|
||||||
VERIFY(!is_completion());
|
|
||||||
return PureResult(move(m_result).downcast<Vector<Value>, Trap>());
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Variant<Vector<Value>, Trap, JS::Completion> m_result;
|
explicit Result(Variant<Vector<Value>, Trap>&& result)
|
||||||
|
: m_result(move(result))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Variant<Vector<Value>, Trap> m_result;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InstantiationError {
|
||||||
|
ByteString error { "Unknown error" };
|
||||||
|
Optional<Trap> relevant_trap {};
|
||||||
};
|
};
|
||||||
|
|
||||||
using ExternValue = Variant<FunctionAddress, TableAddress, MemoryAddress, GlobalAddress>;
|
using ExternValue = Variant<FunctionAddress, TableAddress, MemoryAddress, GlobalAddress>;
|
||||||
|
@ -628,6 +620,10 @@ private:
|
||||||
|
|
||||||
using InstantiationResult = AK::ErrorOr<NonnullOwnPtr<ModuleInstance>, InstantiationError>;
|
using InstantiationResult = AK::ErrorOr<NonnullOwnPtr<ModuleInstance>, InstantiationError>;
|
||||||
|
|
||||||
|
struct HostVisitOps {
|
||||||
|
Function<void(ExternallyManagedTrap&)> visit_trap;
|
||||||
|
};
|
||||||
|
|
||||||
class AbstractMachine {
|
class AbstractMachine {
|
||||||
public:
|
public:
|
||||||
explicit AbstractMachine() = default;
|
explicit AbstractMachine() = default;
|
||||||
|
@ -644,11 +640,38 @@ public:
|
||||||
|
|
||||||
void enable_instruction_count_limit() { m_should_limit_instruction_count = true; }
|
void enable_instruction_count_limit() { m_should_limit_instruction_count = true; }
|
||||||
|
|
||||||
|
void visit_external_resources(HostVisitOps const&);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
class InterpreterHandle {
|
||||||
|
public:
|
||||||
|
explicit InterpreterHandle(AbstractMachine& machine, Interpreter& interpreter)
|
||||||
|
: m_machine(machine)
|
||||||
|
, m_interpreter(interpreter)
|
||||||
|
{
|
||||||
|
m_machine.m_active_interpreters.set(&m_interpreter);
|
||||||
|
}
|
||||||
|
|
||||||
|
~InterpreterHandle()
|
||||||
|
{
|
||||||
|
m_machine.m_active_interpreters.remove(&m_interpreter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
AbstractMachine& m_machine;
|
||||||
|
Interpreter& m_interpreter;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] InterpreterHandle register_scoped(Interpreter& interpreter)
|
||||||
|
{
|
||||||
|
return InterpreterHandle(*this, interpreter);
|
||||||
|
}
|
||||||
|
|
||||||
Optional<InstantiationError> allocate_all_initial_phase(Module const&, ModuleInstance&, Vector<ExternValue>&, Vector<Value>& global_values, Vector<FunctionAddress>& own_functions);
|
Optional<InstantiationError> allocate_all_initial_phase(Module const&, ModuleInstance&, Vector<ExternValue>&, Vector<Value>& global_values, Vector<FunctionAddress>& own_functions);
|
||||||
Optional<InstantiationError> allocate_all_final_phase(Module const&, ModuleInstance&, Vector<Vector<Reference>>& elements);
|
Optional<InstantiationError> allocate_all_final_phase(Module const&, ModuleInstance&, Vector<Vector<Reference>>& elements);
|
||||||
Store m_store;
|
Store m_store;
|
||||||
StackInfo m_stack_info;
|
StackInfo m_stack_info;
|
||||||
|
HashTable<Interpreter*> m_active_interpreters;
|
||||||
bool m_should_limit_instruction_count { false };
|
bool m_should_limit_instruction_count { false };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ void BytecodeInterpreter::interpret(Configuration& configuration)
|
||||||
while (current_ip_value < max_ip_value) {
|
while (current_ip_value < max_ip_value) {
|
||||||
if (should_limit_instruction_count) {
|
if (should_limit_instruction_count) {
|
||||||
if (executed_instructions++ >= Constants::max_allowed_executed_instructions_per_call) [[unlikely]] {
|
if (executed_instructions++ >= Constants::max_allowed_executed_instructions_per_call) [[unlikely]] {
|
||||||
m_trap = Trap { "Exceeded maximum allowed number of instructions" };
|
m_trap = Trap::from_string("Exceeded maximum allowed number of instructions");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ void BytecodeInterpreter::load_and_push(Configuration& configuration, Instructio
|
||||||
auto base = entry.to<i32>();
|
auto base = entry.to<i32>();
|
||||||
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
|
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
|
||||||
if (instance_address + sizeof(ReadType) > memory->size()) {
|
if (instance_address + sizeof(ReadType) > memory->size()) {
|
||||||
m_trap = Trap { "Memory access out of bounds" };
|
m_trap = Trap::from_string("Memory access out of bounds");
|
||||||
dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + sizeof(ReadType), memory->size());
|
dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + sizeof(ReadType), memory->size());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -103,7 +103,7 @@ void BytecodeInterpreter::load_and_push_mxn(Configuration& configuration, Instru
|
||||||
auto base = entry.to<i32>();
|
auto base = entry.to<i32>();
|
||||||
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
|
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
|
||||||
if (instance_address + M * N / 8 > memory->size()) {
|
if (instance_address + M * N / 8 > memory->size()) {
|
||||||
m_trap = Trap { "Memory access out of bounds" };
|
m_trap = Trap::from_string("Memory access out of bounds");
|
||||||
dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + M * N / 8, memory->size());
|
dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + M * N / 8, memory->size());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -131,7 +131,7 @@ void BytecodeInterpreter::load_and_push_lane_n(Configuration& configuration, Ins
|
||||||
auto base = configuration.value_stack().take_last().to<u32>();
|
auto base = configuration.value_stack().take_last().to<u32>();
|
||||||
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + memarg_and_lane.memory.offset;
|
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + memarg_and_lane.memory.offset;
|
||||||
if (instance_address + N / 8 > memory->size()) {
|
if (instance_address + N / 8 > memory->size()) {
|
||||||
m_trap = Trap { "Memory access out of bounds" };
|
m_trap = Trap::from_string("Memory access out of bounds");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto slice = memory->data().bytes().slice(instance_address, N / 8);
|
auto slice = memory->data().bytes().slice(instance_address, N / 8);
|
||||||
|
@ -149,7 +149,7 @@ void BytecodeInterpreter::load_and_push_zero_n(Configuration& configuration, Ins
|
||||||
auto base = configuration.value_stack().take_last().to<u32>();
|
auto base = configuration.value_stack().take_last().to<u32>();
|
||||||
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + memarg_and_lane.offset;
|
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + memarg_and_lane.offset;
|
||||||
if (instance_address + N / 8 > memory->size()) {
|
if (instance_address + N / 8 > memory->size()) {
|
||||||
m_trap = Trap { "Memory access out of bounds" };
|
m_trap = Trap::from_string("Memory access out of bounds");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto slice = memory->data().bytes().slice(instance_address, N / 8);
|
auto slice = memory->data().bytes().slice(instance_address, N / 8);
|
||||||
|
@ -168,7 +168,7 @@ void BytecodeInterpreter::load_and_push_m_splat(Configuration& configuration, In
|
||||||
auto base = entry.to<i32>();
|
auto base = entry.to<i32>();
|
||||||
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
|
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
|
||||||
if (instance_address + M / 8 > memory->size()) {
|
if (instance_address + M / 8 > memory->size()) {
|
||||||
m_trap = Trap { "Memory access out of bounds" };
|
m_trap = Trap::from_string("Memory access out of bounds");
|
||||||
dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + M / 8, memory->size());
|
dbgln("LibWasm: Memory access out of bounds (expected {} to be less than or equal to {})", instance_address + M / 8, memory->size());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -238,7 +238,7 @@ void BytecodeInterpreter::call_address(Configuration& configuration, FunctionAdd
|
||||||
|
|
||||||
configuration.value_stack().remove(configuration.value_stack().size() - span.size(), span.size());
|
configuration.value_stack().remove(configuration.value_stack().size() - span.size(), span.size());
|
||||||
|
|
||||||
Result result { Trap { ""sv } };
|
Result result { Trap::from_string("") };
|
||||||
if (instance->has<WasmFunction>()) {
|
if (instance->has<WasmFunction>()) {
|
||||||
CallFrameHandle handle { *this, configuration };
|
CallFrameHandle handle { *this, configuration };
|
||||||
result = configuration.call(*this, address, move(args));
|
result = configuration.call(*this, address, move(args));
|
||||||
|
@ -251,11 +251,6 @@ void BytecodeInterpreter::call_address(Configuration& configuration, FunctionAdd
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.is_completion()) {
|
|
||||||
m_trap = move(result.completion());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
configuration.value_stack().ensure_capacity(configuration.value_stack().size() + result.values().size());
|
configuration.value_stack().ensure_capacity(configuration.value_stack().size() + result.values().size());
|
||||||
for (auto& entry : result.values().in_reverse())
|
for (auto& entry : result.values().in_reverse())
|
||||||
configuration.value_stack().unchecked_append(entry);
|
configuration.value_stack().unchecked_append(entry);
|
||||||
|
@ -361,7 +356,7 @@ void BytecodeInterpreter::store_to_memory(Configuration& configuration, Instruct
|
||||||
Checked addition { instance_address };
|
Checked addition { instance_address };
|
||||||
addition += data.size();
|
addition += data.size();
|
||||||
if (addition.has_overflow() || addition.value() > memory->size()) {
|
if (addition.has_overflow() || addition.value() > memory->size()) {
|
||||||
m_trap = Trap { "Memory access out of bounds" };
|
m_trap = Trap::from_string("Memory access out of bounds");
|
||||||
dbgln("LibWasm: Memory access out of bounds (expected 0 <= {} and {} <= {})", instance_address, instance_address + data.size(), memory->size());
|
dbgln("LibWasm: Memory access out of bounds (expected 0 <= {} and {} <= {})", instance_address, instance_address + data.size(), memory->size());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -376,7 +371,7 @@ T BytecodeInterpreter::read_value(ReadonlyBytes data)
|
||||||
auto value_or_error = stream.read_value<LittleEndian<T>>();
|
auto value_or_error = stream.read_value<LittleEndian<T>>();
|
||||||
if (value_or_error.is_error()) {
|
if (value_or_error.is_error()) {
|
||||||
dbgln("Read from {} failed", data.data());
|
dbgln("Read from {} failed", data.data());
|
||||||
m_trap = Trap { "Read from memory failed" };
|
m_trap = Trap::from_string("Read from memory failed");
|
||||||
}
|
}
|
||||||
return value_or_error.release_value();
|
return value_or_error.release_value();
|
||||||
}
|
}
|
||||||
|
@ -387,7 +382,7 @@ float BytecodeInterpreter::read_value<float>(ReadonlyBytes data)
|
||||||
FixedMemoryStream stream { data };
|
FixedMemoryStream stream { data };
|
||||||
auto raw_value_or_error = stream.read_value<LittleEndian<u32>>();
|
auto raw_value_or_error = stream.read_value<LittleEndian<u32>>();
|
||||||
if (raw_value_or_error.is_error())
|
if (raw_value_or_error.is_error())
|
||||||
m_trap = Trap { "Read from memory failed" };
|
m_trap = Trap::from_string("Read from memory failed");
|
||||||
auto raw_value = raw_value_or_error.release_value();
|
auto raw_value = raw_value_or_error.release_value();
|
||||||
return bit_cast<float>(static_cast<u32>(raw_value));
|
return bit_cast<float>(static_cast<u32>(raw_value));
|
||||||
}
|
}
|
||||||
|
@ -398,7 +393,7 @@ double BytecodeInterpreter::read_value<double>(ReadonlyBytes data)
|
||||||
FixedMemoryStream stream { data };
|
FixedMemoryStream stream { data };
|
||||||
auto raw_value_or_error = stream.read_value<LittleEndian<u64>>();
|
auto raw_value_or_error = stream.read_value<LittleEndian<u64>>();
|
||||||
if (raw_value_or_error.is_error())
|
if (raw_value_or_error.is_error())
|
||||||
m_trap = Trap { "Read from memory failed" };
|
m_trap = Trap::from_string("Read from memory failed");
|
||||||
auto raw_value = raw_value_or_error.release_value();
|
auto raw_value = raw_value_or_error.release_value();
|
||||||
return bit_cast<double>(static_cast<u64>(raw_value));
|
return bit_cast<double>(static_cast<u64>(raw_value));
|
||||||
}
|
}
|
||||||
|
@ -409,7 +404,7 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
||||||
|
|
||||||
switch (instruction.opcode().value()) {
|
switch (instruction.opcode().value()) {
|
||||||
case Instructions::unreachable.value():
|
case Instructions::unreachable.value():
|
||||||
m_trap = Trap { "Unreachable" };
|
m_trap = Trap::from_string("Unreachable");
|
||||||
return;
|
return;
|
||||||
case Instructions::nop.value():
|
case Instructions::nop.value():
|
||||||
return;
|
return;
|
||||||
|
@ -1659,7 +1654,7 @@ void DebuggerBytecodeInterpreter::interpret_instruction(Configuration& configura
|
||||||
if (pre_interpret_hook) {
|
if (pre_interpret_hook) {
|
||||||
auto result = pre_interpret_hook(configuration, ip, instruction);
|
auto result = pre_interpret_hook(configuration, ip, instruction);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
m_trap = Trap { "Trapped by user request" };
|
m_trap = Trap::from_string("Trapped by user request");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1669,7 +1664,7 @@ void DebuggerBytecodeInterpreter::interpret_instruction(Configuration& configura
|
||||||
if (post_interpret_hook) {
|
if (post_interpret_hook) {
|
||||||
auto result = post_interpret_hook(configuration, ip, instruction, *this);
|
auto result = post_interpret_hook(configuration, ip, instruction, *this);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
m_trap = Trap { "Trapped by user request" };
|
m_trap = Trap::from_string("Trapped by user request");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,14 +22,17 @@ struct BytecodeInterpreter : public Interpreter {
|
||||||
|
|
||||||
virtual ~BytecodeInterpreter() override = default;
|
virtual ~BytecodeInterpreter() override = default;
|
||||||
virtual bool did_trap() const final { return !m_trap.has<Empty>(); }
|
virtual bool did_trap() const final { return !m_trap.has<Empty>(); }
|
||||||
virtual ByteString trap_reason() const final
|
virtual Trap trap() const final
|
||||||
{
|
{
|
||||||
return m_trap.visit(
|
return m_trap.get<Trap>();
|
||||||
[](Empty) -> ByteString { VERIFY_NOT_REACHED(); },
|
|
||||||
[](Trap const& trap) { return trap.reason; },
|
|
||||||
[](JS::Completion const& completion) { return completion.value().to_string_without_side_effects().to_byte_string(); });
|
|
||||||
}
|
}
|
||||||
virtual void clear_trap() final { m_trap = Empty {}; }
|
virtual void clear_trap() final { m_trap = Empty {}; }
|
||||||
|
virtual void visit_external_resources(HostVisitOps const& host) override
|
||||||
|
{
|
||||||
|
if (auto ptr = m_trap.get_pointer<Trap>())
|
||||||
|
if (auto data = ptr->data.get_pointer<ExternallyManagedTrap>())
|
||||||
|
host.visit_trap(*data);
|
||||||
|
}
|
||||||
|
|
||||||
struct CallFrameHandle {
|
struct CallFrameHandle {
|
||||||
explicit CallFrameHandle(BytecodeInterpreter& interpreter, Configuration& configuration)
|
explicit CallFrameHandle(BytecodeInterpreter& interpreter, Configuration& configuration)
|
||||||
|
@ -82,11 +85,11 @@ protected:
|
||||||
ALWAYS_INLINE bool trap_if_not(bool value, StringView reason)
|
ALWAYS_INLINE bool trap_if_not(bool value, StringView reason)
|
||||||
{
|
{
|
||||||
if (!value)
|
if (!value)
|
||||||
m_trap = Trap { reason };
|
m_trap = Trap { ByteString(reason) };
|
||||||
return !m_trap.has<Empty>();
|
return !m_trap.has<Empty>();
|
||||||
}
|
}
|
||||||
|
|
||||||
Variant<Trap, JS::Completion, Empty> m_trap;
|
Variant<Trap, Empty> m_trap;
|
||||||
StackInfo const& m_stack_info;
|
StackInfo const& m_stack_info;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ Result Configuration::call(Interpreter& interpreter, FunctionAddress address, Ve
|
||||||
{
|
{
|
||||||
auto* function = m_store.get(address);
|
auto* function = m_store.get(address);
|
||||||
if (!function)
|
if (!function)
|
||||||
return Trap {};
|
return Trap::from_string("Attempt to call nonexistent function by address");
|
||||||
if (auto* wasm_function = function->get_pointer<WasmFunction>()) {
|
if (auto* wasm_function = function->get_pointer<WasmFunction>()) {
|
||||||
Vector<Value> locals = move(arguments);
|
Vector<Value> locals = move(arguments);
|
||||||
locals.ensure_capacity(locals.size() + wasm_function->code().func().locals().size());
|
locals.ensure_capacity(locals.size() + wasm_function->code().func().locals().size());
|
||||||
|
@ -50,7 +50,7 @@ Result Configuration::execute(Interpreter& interpreter)
|
||||||
{
|
{
|
||||||
interpreter.interpret(*this);
|
interpreter.interpret(*this);
|
||||||
if (interpreter.did_trap())
|
if (interpreter.did_trap())
|
||||||
return Trap { interpreter.trap_reason() };
|
return interpreter.trap();
|
||||||
|
|
||||||
Vector<Value> results;
|
Vector<Value> results;
|
||||||
results.ensure_capacity(frame().arity());
|
results.ensure_capacity(frame().arity());
|
||||||
|
|
|
@ -13,9 +13,10 @@ namespace Wasm {
|
||||||
struct Interpreter {
|
struct Interpreter {
|
||||||
virtual ~Interpreter() = default;
|
virtual ~Interpreter() = default;
|
||||||
virtual void interpret(Configuration&) = 0;
|
virtual void interpret(Configuration&) = 0;
|
||||||
|
virtual Trap trap() const = 0;
|
||||||
virtual bool did_trap() const = 0;
|
virtual bool did_trap() const = 0;
|
||||||
virtual ByteString trap_reason() const = 0;
|
|
||||||
virtual void clear_trap() = 0;
|
virtual void clear_trap() = 0;
|
||||||
|
virtual void visit_external_resources(HostVisitOps const&) { }
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,6 @@ if (NOT WIN32)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
serenity_lib(LibWasm wasm)
|
serenity_lib(LibWasm wasm)
|
||||||
target_link_libraries(LibWasm PRIVATE LibCore LibGC LibJS)
|
target_link_libraries(LibWasm PRIVATE LibCore)
|
||||||
|
|
||||||
include(wasm_spec_tests)
|
include(wasm_spec_tests)
|
||||||
|
|
|
@ -59,6 +59,10 @@ void visit_edges(JS::Object& object, JS::Cell::Visitor& visitor)
|
||||||
visitor.visit(cache.imported_objects());
|
visitor.visit(cache.imported_objects());
|
||||||
visitor.visit(cache.extern_values());
|
visitor.visit(cache.extern_values());
|
||||||
visitor.visit(cache.global_instances());
|
visitor.visit(cache.global_instances());
|
||||||
|
cache.abstract_machine().visit_external_resources({ .visit_trap = [&visitor](Wasm::ExternallyManagedTrap const& trap) {
|
||||||
|
auto& completion = trap.unsafe_external_object_as<JS::Completion>();
|
||||||
|
visitor.visit(completion.value());
|
||||||
|
} });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +160,32 @@ WebIDL::ExceptionOr<GC::Ref<WebIDL::Promise>> instantiate_streaming(JS::VM& vm,
|
||||||
|
|
||||||
namespace Detail {
|
namespace Detail {
|
||||||
|
|
||||||
|
#define TRY_OR_RETURN_TRAP(...) \
|
||||||
|
({ \
|
||||||
|
/* Ignore -Wshadow to allow nesting the macro. */ \
|
||||||
|
AK_IGNORE_DIAGNOSTIC("-Wshadow", \
|
||||||
|
auto&& _temporary_result = (__VA_ARGS__)); \
|
||||||
|
static_assert(!::AK::Detail::IsLvalueReference<decltype(_temporary_result.release_value())>, \
|
||||||
|
"Do not return a reference from a fallible expression"); \
|
||||||
|
if (_temporary_result.is_error()) [[unlikely]] \
|
||||||
|
return Wasm::Trap::from_external_object(_temporary_result.release_error()); \
|
||||||
|
_temporary_result.release_value(); \
|
||||||
|
})
|
||||||
|
|
||||||
|
#define TRY_OR_RETURN_OOM_TRAP(vm, ...) \
|
||||||
|
({ \
|
||||||
|
/* Ignore -Wshadow to allow nesting the macro. */ \
|
||||||
|
AK_IGNORE_DIAGNOSTIC("-Wshadow", \
|
||||||
|
auto&& _temporary_result = (__VA_ARGS__)); \
|
||||||
|
if (_temporary_result.is_error()) { \
|
||||||
|
VERIFY(_temporary_result.error().code() == ENOMEM); \
|
||||||
|
return Wasm::Trap::from_external_object((vm).throw_completion<JS::InternalError>((vm).error_message(::JS::VM::ErrorMessage::OutOfMemory))); \
|
||||||
|
} \
|
||||||
|
static_assert(!::AK::Detail::IsLvalueReference<decltype(_temporary_result.release_value())>, \
|
||||||
|
"Do not return a reference from a fallible expression"); \
|
||||||
|
_temporary_result.release_value(); \
|
||||||
|
})
|
||||||
|
|
||||||
JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS::VM& vm, Wasm::Module const& module, GC::Ptr<JS::Object> import_object)
|
JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS::VM& vm, Wasm::Module const& module, GC::Ptr<JS::Object> import_object)
|
||||||
{
|
{
|
||||||
Wasm::Linker linker { module };
|
Wasm::Linker linker { module };
|
||||||
|
@ -216,28 +246,28 @@ JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS
|
||||||
++index;
|
++index;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto result = TRY(JS::call(vm, function, JS::js_undefined(), argument_values.span()));
|
auto result = TRY_OR_RETURN_TRAP(JS::call(vm, function, JS::js_undefined(), argument_values.span()));
|
||||||
if (type.results().is_empty())
|
if (type.results().is_empty())
|
||||||
return Wasm::Result { Vector<Wasm::Value> {} };
|
return Wasm::Result { Vector<Wasm::Value> {} };
|
||||||
|
|
||||||
if (type.results().size() == 1)
|
if (type.results().size() == 1)
|
||||||
return Wasm::Result { Vector<Wasm::Value> { TRY(to_webassembly_value(vm, result, type.results().first())) } };
|
return Wasm::Result { Vector<Wasm::Value> { TRY_OR_RETURN_TRAP(to_webassembly_value(vm, result, type.results().first())) } };
|
||||||
|
|
||||||
auto method = TRY(result.get_method(vm, vm.names.iterator));
|
auto method = TRY_OR_RETURN_TRAP(result.get_method(vm, vm.names.iterator));
|
||||||
if (method == JS::js_undefined())
|
if (method == JS::js_undefined())
|
||||||
return vm.throw_completion<JS::TypeError>(JS::ErrorType::NotIterable, result.to_string_without_side_effects());
|
return Wasm::Trap::from_external_object(vm.throw_completion<JS::TypeError>(JS::ErrorType::NotIterable, result.to_string_without_side_effects()));
|
||||||
|
|
||||||
auto values = TRY(JS::iterator_to_list(vm, TRY(JS::get_iterator_from_method(vm, result, *method))));
|
auto values = TRY_OR_RETURN_TRAP(JS::iterator_to_list(vm, TRY_OR_RETURN_TRAP(JS::get_iterator_from_method(vm, result, *method))));
|
||||||
|
|
||||||
if (values.size() != type.results().size())
|
if (values.size() != type.results().size())
|
||||||
return vm.throw_completion<JS::TypeError>(ByteString::formatted("Invalid number of return values for multi-value wasm return of {} objects", type.results().size()));
|
return Wasm::Trap::from_external_object(vm.throw_completion<JS::TypeError>(ByteString::formatted("Invalid number of return values for multi-value wasm return of {} objects", type.results().size())));
|
||||||
|
|
||||||
Vector<Wasm::Value> wasm_values;
|
Vector<Wasm::Value> wasm_values;
|
||||||
TRY_OR_THROW_OOM(vm, wasm_values.try_ensure_capacity(values.size()));
|
TRY_OR_RETURN_OOM_TRAP(vm, wasm_values.try_ensure_capacity(values.size()));
|
||||||
|
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
for (auto& value : values)
|
for (auto& value : values)
|
||||||
wasm_values.append(TRY(to_webassembly_value(vm, value, type.results()[i++])));
|
wasm_values.append(TRY_OR_RETURN_TRAP(to_webassembly_value(vm, value, type.results()[i++])));
|
||||||
|
|
||||||
return Wasm::Result { move(wasm_values) };
|
return Wasm::Result { move(wasm_values) };
|
||||||
},
|
},
|
||||||
|
@ -415,8 +445,11 @@ JS::NativeFunction* create_native_function(JS::VM& vm, Wasm::FunctionAddress add
|
||||||
auto& cache = get_cache(realm);
|
auto& cache = get_cache(realm);
|
||||||
auto result = cache.abstract_machine().invoke(address, move(values));
|
auto result = cache.abstract_machine().invoke(address, move(values));
|
||||||
// FIXME: Use the convoluted mapping of errors defined in the spec.
|
// FIXME: Use the convoluted mapping of errors defined in the spec.
|
||||||
if (result.is_trap())
|
if (result.is_trap()) {
|
||||||
return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("Wasm execution trapped (WIP): {}", result.trap().reason)));
|
if (auto ptr = result.trap().data.get_pointer<Wasm::ExternallyManagedTrap>())
|
||||||
|
return ptr->unsafe_external_object_as<JS::Completion>();
|
||||||
|
return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("Wasm execution trapped (WIP): {}", result.trap().format())));
|
||||||
|
}
|
||||||
|
|
||||||
if (result.values().is_empty())
|
if (result.values().is_empty())
|
||||||
return JS::js_undefined();
|
return JS::js_undefined();
|
||||||
|
|
|
@ -471,7 +471,7 @@ if (ASSERT_FAIL_HAS_INT OR EMSCRIPTEN)
|
||||||
target_compile_definitions(test262-runner PRIVATE ASSERT_FAIL_HAS_INT)
|
target_compile_definitions(test262-runner PRIVATE ASSERT_FAIL_HAS_INT)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
lagom_utility(wasm SOURCES ../../Utilities/wasm.cpp LIBS LibFileSystem LibWasm LibLine LibMain LibJS)
|
lagom_utility(wasm SOURCES ../../Utilities/wasm.cpp LIBS LibFileSystem LibWasm LibLine LibMain)
|
||||||
lagom_utility(xml SOURCES ../../Utilities/xml.cpp LIBS LibFileSystem LibMain LibXML LibURL)
|
lagom_utility(xml SOURCES ../../Utilities/xml.cpp LIBS LibFileSystem LibMain LibXML LibURL)
|
||||||
|
|
||||||
include(CTest)
|
include(CTest)
|
||||||
|
|
|
@ -401,11 +401,11 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke)
|
||||||
|
|
||||||
auto functype = WebAssemblyModule::machine().store().get(function_address)->visit([&](auto& func) { return func.type(); });
|
auto functype = WebAssemblyModule::machine().store().get(function_address)->visit([&](auto& func) { return func.type(); });
|
||||||
auto result = WebAssemblyModule::machine().invoke(function_address, arguments);
|
auto result = WebAssemblyModule::machine().invoke(function_address, arguments);
|
||||||
if (result.is_trap())
|
if (result.is_trap()) {
|
||||||
return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("Execution trapped: {}", result.trap().reason)));
|
if (auto ptr = result.trap().data.get_pointer<Wasm::ExternallyManagedTrap>())
|
||||||
|
return ptr->unsafe_external_object_as<JS::Completion>();
|
||||||
if (result.is_completion())
|
return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("Execution trapped: {}", result.trap().format())));
|
||||||
return result.completion();
|
}
|
||||||
|
|
||||||
if (result.values().is_empty())
|
if (result.values().is_empty())
|
||||||
return JS::js_null();
|
return JS::js_null();
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
#include <LibWasm/Printer/Printer.h>
|
#include <LibWasm/Printer/Printer.h>
|
||||||
#include <LibWasm/Types.h>
|
#include <LibWasm/Types.h>
|
||||||
#include <LibWasm/Wasi.h>
|
#include <LibWasm/Wasi.h>
|
||||||
|
#include <math.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
|
@ -264,7 +265,7 @@ static bool post_interpret_hook(Wasm::Configuration&, Wasm::InstructionPointer&
|
||||||
g_continue = false;
|
g_continue = false;
|
||||||
warnln("Trapped when executing ip={}", ip);
|
warnln("Trapped when executing ip={}", ip);
|
||||||
g_printer->print(instr);
|
g_printer->print(instr);
|
||||||
warnln("Trap reason: {}", interpreter.trap_reason());
|
warnln("Trap reason: {}", interpreter.trap().format());
|
||||||
const_cast<Wasm::Interpreter&>(interpreter).clear_trap();
|
const_cast<Wasm::Interpreter&>(interpreter).clear_trap();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -446,13 +447,13 @@ static bool pre_interpret_hook(Wasm::Configuration& config, Wasm::InstructionPoi
|
||||||
if (!ok)
|
if (!ok)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Wasm::Result result { Wasm::Trap {} };
|
Wasm::Result result { Wasm::Trap::from_string("") };
|
||||||
{
|
{
|
||||||
Wasm::BytecodeInterpreter::CallFrameHandle handle { g_interpreter, config };
|
Wasm::BytecodeInterpreter::CallFrameHandle handle { g_interpreter, config };
|
||||||
result = config.call(g_interpreter, *address, move(values)).assert_wasm_result();
|
result = config.call(g_interpreter, *address, move(values));
|
||||||
}
|
}
|
||||||
if (result.is_trap()) {
|
if (result.is_trap()) {
|
||||||
warnln("Execution trapped: {}", result.trap().reason);
|
warnln("Execution trapped: {}", result.trap().format());
|
||||||
} else {
|
} else {
|
||||||
if (!result.values().is_empty())
|
if (!result.values().is_empty())
|
||||||
warnln("Returned:");
|
warnln("Returned:");
|
||||||
|
@ -833,15 +834,16 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||||
outln();
|
outln();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto result = machine.invoke(g_interpreter, run_address.value(), move(values)).assert_wasm_result();
|
auto result = machine.invoke(g_interpreter, run_address.value(), move(values));
|
||||||
|
|
||||||
if (debug)
|
if (debug)
|
||||||
launch_repl();
|
launch_repl();
|
||||||
|
|
||||||
if (result.is_trap()) {
|
if (result.is_trap()) {
|
||||||
if (result.trap().reason.starts_with("exit:"sv))
|
auto trap_reason = result.trap().format();
|
||||||
return -result.trap().reason.substring_view(5).to_number<i32>().value_or(-1);
|
if (trap_reason.starts_with("exit:"sv))
|
||||||
warnln("Execution trapped: {}", result.trap().reason);
|
return -trap_reason.substring_view(5).to_number<i32>().value_or(-1);
|
||||||
|
warnln("Execution trapped: {}", trap_reason);
|
||||||
} else {
|
} else {
|
||||||
if (!result.values().is_empty())
|
if (!result.values().is_empty())
|
||||||
warnln("Returned:");
|
warnln("Returned:");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue