LibWasm: Remove type information from Value
Gets fib(30) from 380ms to 340ms.
This commit is contained in:
parent
a2448308fd
commit
a58704296c
Notes:
github-actions[bot]
2024-08-06 23:11:13 +00:00
Author: https://github.com/dzfrias Commit: https://github.com/LadybirdBrowser/ladybird/commit/a58704296cd Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/960 Reviewed-by: https://github.com/alimpfard
12 changed files with 349 additions and 287 deletions
|
@ -295,16 +295,27 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::get_export)
|
|||
if (auto ptr = value.get_pointer<Wasm::FunctionAddress>())
|
||||
return JS::Value(static_cast<unsigned long>(ptr->value()));
|
||||
if (auto v = value.get_pointer<Wasm::GlobalAddress>()) {
|
||||
return m_machine.store().get(*v)->value().value().visit(
|
||||
[&](auto const& value) -> JS::Value { return JS::Value(static_cast<double>(value)); },
|
||||
[&](i32 value) { return JS::Value(static_cast<double>(value)); },
|
||||
[&](i64 value) -> JS::Value { return JS::BigInt::create(vm, Crypto::SignedBigInteger { value }); },
|
||||
[&](u128 value) -> JS::Value { return JS::BigInt::create(vm, Crypto::SignedBigInteger::import_data(bit_cast<u8 const*>(&value), sizeof(value))); },
|
||||
[&](Wasm::Reference const& reference) -> JS::Value {
|
||||
return reference.ref().visit(
|
||||
[&](Wasm::Reference::Null const&) -> JS::Value { return JS::js_null(); },
|
||||
[&](auto const& ref) -> JS::Value { return JS::Value(static_cast<double>(ref.address.value())); });
|
||||
});
|
||||
auto global = m_machine.store().get(*v);
|
||||
switch (global->type().type().kind()) {
|
||||
case Wasm::ValueType::I32:
|
||||
return JS::Value(static_cast<double>(global->value().to<i32>()));
|
||||
case Wasm::ValueType::I64:
|
||||
return JS::BigInt::create(vm, Crypto::SignedBigInteger { global->value().to<i64>() });
|
||||
case Wasm::ValueType::F32:
|
||||
return JS::Value(static_cast<double>(global->value().to<float>()));
|
||||
case Wasm::ValueType::F64:
|
||||
return JS::Value(global->value().to<double>());
|
||||
case Wasm::ValueType::V128: {
|
||||
auto value = global->value().to<u128>();
|
||||
return JS::BigInt::create(vm, Crypto::SignedBigInteger::import_data(bit_cast<u8 const*>(&value), sizeof(u128)));
|
||||
}
|
||||
case Wasm::ValueType::FunctionReference:
|
||||
case Wasm::ValueType::ExternReference:
|
||||
auto ref = global->value().to<Wasm::Reference>();
|
||||
return ref.ref().visit(
|
||||
[&](Wasm::Reference::Null const&) -> JS::Value { return JS::js_null(); },
|
||||
[&](auto const& ref) -> JS::Value { return JS::Value(static_cast<double>(ref.address.value())); });
|
||||
}
|
||||
}
|
||||
return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("'{}' does not refer to a function or a global", name)));
|
||||
}
|
||||
|
@ -336,14 +347,14 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke)
|
|||
double_value = TRY(argument.to_double(vm));
|
||||
switch (param.kind()) {
|
||||
case Wasm::ValueType::Kind::I32:
|
||||
arguments.append(Wasm::Value(param, static_cast<i64>(double_value)));
|
||||
arguments.append(Wasm::Value(static_cast<i64>(double_value)));
|
||||
break;
|
||||
case Wasm::ValueType::Kind::I64:
|
||||
if (argument.is_bigint()) {
|
||||
auto value = TRY(argument.to_bigint_int64(vm));
|
||||
arguments.append(Wasm::Value(param, value));
|
||||
arguments.append(Wasm::Value(value));
|
||||
} else {
|
||||
arguments.append(Wasm::Value(param, static_cast<i64>(double_value)));
|
||||
arguments.append(Wasm::Value(static_cast<i64>(double_value)));
|
||||
}
|
||||
break;
|
||||
case Wasm::ValueType::Kind::F32:
|
||||
|
@ -352,9 +363,9 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke)
|
|||
case Wasm::ValueType::Kind::F64:
|
||||
if (argument.is_bigint()) {
|
||||
auto value = TRY(argument.to_bigint_uint64(vm));
|
||||
arguments.append(Wasm::Value(param, bit_cast<double>(value)));
|
||||
arguments.append(Wasm::Value(bit_cast<double>(value)));
|
||||
} else {
|
||||
arguments.append(Wasm::Value(param, double_value));
|
||||
arguments.append(Wasm::Value(double_value));
|
||||
}
|
||||
break;
|
||||
case Wasm::ValueType::Kind::V128: {
|
||||
|
@ -385,6 +396,7 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke)
|
|||
}
|
||||
}
|
||||
|
||||
auto functype = WebAssemblyModule::machine().store().get(function_address)->visit([&](auto& func) { return func.type(); });
|
||||
auto result = WebAssemblyModule::machine().invoke(function_address, arguments);
|
||||
if (result.is_trap())
|
||||
return vm.throw_completion<JS::TypeError>(TRY_OR_THROW_OOM(vm, String::formatted("Execution trapped: {}", result.trap().reason)));
|
||||
|
@ -395,30 +407,36 @@ JS_DEFINE_NATIVE_FUNCTION(WebAssemblyModule::wasm_invoke)
|
|||
if (result.values().is_empty())
|
||||
return JS::js_null();
|
||||
|
||||
auto to_js_value = [&](Wasm::Value const& value) {
|
||||
return value.value().visit(
|
||||
// For floating point values, we're testing with their bit representation, so we bit_cast them
|
||||
[](f32 value) { return JS::Value(static_cast<double>(bit_cast<u32>(value))); },
|
||||
[&](f64 value) { return JS::Value(JS::BigInt::create(vm, Crypto::SignedBigInteger { Crypto::UnsignedBigInteger { bit_cast<u64>(value) } })); },
|
||||
[](i32 value) { return JS::Value(static_cast<double>(value)); },
|
||||
[&](i64 value) { return JS::Value(JS::BigInt::create(vm, Crypto::SignedBigInteger { value })); },
|
||||
[&](u128 value) {
|
||||
// FIXME: remove the MUST here
|
||||
auto buf = MUST(JS::ArrayBuffer::create(*vm.current_realm(), 16));
|
||||
memcpy(buf->buffer().data(), value.bytes().data(), 16);
|
||||
return JS::Value(buf);
|
||||
},
|
||||
[](Wasm::Reference const& reference) {
|
||||
return reference.ref().visit(
|
||||
[](Wasm::Reference::Null const&) { return JS::js_null(); },
|
||||
[](auto const& ref) { return JS::Value(static_cast<double>(ref.address.value())); });
|
||||
});
|
||||
auto to_js_value = [&](Wasm::Value const& value, Wasm::ValueType type) {
|
||||
switch (type.kind()) {
|
||||
case Wasm::ValueType::I32:
|
||||
return JS::Value(static_cast<double>(value.to<i32>()));
|
||||
case Wasm::ValueType::I64:
|
||||
return JS::Value(JS::BigInt::create(vm, Crypto::SignedBigInteger { value.to<i64>() }));
|
||||
case Wasm::ValueType::F32:
|
||||
return JS::Value(static_cast<double>(bit_cast<u32>(value.to<float>())));
|
||||
case Wasm::ValueType::F64:
|
||||
return JS::Value(JS::BigInt::create(vm, Crypto::SignedBigInteger { Crypto::UnsignedBigInteger { bit_cast<u64>(value.to<double>()) } }));
|
||||
case Wasm::ValueType::V128: {
|
||||
u128 val = value.to<u128>();
|
||||
// FIXME: remove the MUST here
|
||||
auto buf = MUST(JS::ArrayBuffer::create(*vm.current_realm(), 16));
|
||||
memcpy(buf->buffer().data(), val.bytes().data(), 16);
|
||||
return JS::Value(buf);
|
||||
}
|
||||
case Wasm::ValueType::FunctionReference:
|
||||
case Wasm::ValueType::ExternReference:
|
||||
return (value.to<Wasm::Reference>()).ref().visit([&](Wasm::Reference::Null) { return JS::js_null(); }, [&](auto const& ref) { return JS::Value(static_cast<double>(ref.address.value())); });
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
};
|
||||
|
||||
if (result.values().size() == 1)
|
||||
return to_js_value(result.values().first());
|
||||
return to_js_value(result.values().first(), functype.results().first());
|
||||
|
||||
size_t i = 0;
|
||||
return JS::Array::create_from<Wasm::Value>(*vm.current_realm(), result.values(), [&](Wasm::Value value) {
|
||||
return to_js_value(value);
|
||||
auto value_type = type->results()[i++];
|
||||
return to_js_value(value, value_type);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@ Optional<MemoryAddress> Store::allocate(MemoryType const& type)
|
|||
Optional<GlobalAddress> Store::allocate(GlobalType const& type, Value value)
|
||||
{
|
||||
GlobalAddress address { m_globals.size() };
|
||||
m_globals.append(GlobalInstance { move(value), type.is_mutable() });
|
||||
m_globals.append(GlobalInstance { value, type.is_mutable(), type.type() });
|
||||
return address;
|
||||
}
|
||||
|
||||
|
@ -138,7 +138,6 @@ ErrorOr<void, ValidationError> AbstractMachine::validate(Module& module)
|
|||
|
||||
return {};
|
||||
}
|
||||
|
||||
InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<ExternValue> externs)
|
||||
{
|
||||
if (auto result = validate(const_cast<Module&>(module)); result.is_error())
|
||||
|
@ -267,7 +266,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
|||
|
||||
for (auto& value : result.values()) {
|
||||
auto reference = value.to<Reference>();
|
||||
references.append(reference.release_value());
|
||||
references.append(reference);
|
||||
}
|
||||
}
|
||||
elements.append(move(references));
|
||||
|
@ -300,8 +299,6 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
|||
if (result.is_trap())
|
||||
return InstantiationError { ByteString::formatted("Element section initialisation trapped: {}", result.trap().reason) };
|
||||
auto d = result.values().first().to<i32>();
|
||||
if (!d.has_value())
|
||||
return InstantiationError { "Element section initialisation returned invalid table initial offset" };
|
||||
auto table_instance = m_store.get(main_module_instance.tables()[active_ptr->index.value()]);
|
||||
if (current_index >= main_module_instance.elements().size())
|
||||
return InstantiationError { "Invalid element referenced by active element segment" };
|
||||
|
@ -309,14 +306,14 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
|||
return InstantiationError { "Invalid element referenced by active element segment" };
|
||||
|
||||
Checked<size_t> total_size = elem_instance->references().size();
|
||||
total_size.saturating_add(d.value());
|
||||
total_size.saturating_add(d);
|
||||
|
||||
if (total_size.value() > table_instance->elements().size())
|
||||
return InstantiationError { "Table instantiation out of bounds" };
|
||||
|
||||
size_t i = 0;
|
||||
for (auto it = elem_instance->references().begin(); it < elem_instance->references().end(); ++i, ++it)
|
||||
table_instance->elements()[i + d.value()] = *it;
|
||||
table_instance->elements()[i + d] = *it;
|
||||
// Drop element
|
||||
*m_store.get(main_module_instance.elements()[current_index]) = ElementInstance(elem_instance->type(), {});
|
||||
}
|
||||
|
@ -336,10 +333,7 @@ InstantiationResult AbstractMachine::instantiate(Module const& module, Vector<Ex
|
|||
auto result = config.execute(interpreter).assert_wasm_result();
|
||||
if (result.is_trap())
|
||||
return InstantiationError { ByteString::formatted("Data section initialisation trapped: {}", result.trap().reason) };
|
||||
size_t offset = TRY(result.values().first().value().visit(
|
||||
[&](auto const& value) { return ErrorOr<size_t, InstantiationError> { value }; },
|
||||
[&](u128 const&) { return ErrorOr<size_t, InstantiationError> { InstantiationError { "Data segment offset returned a vector type"sv } }; },
|
||||
[&](Reference const&) { return ErrorOr<size_t, InstantiationError> { InstantiationError { "Data segment offset returned a reference type"sv } }; }));
|
||||
size_t offset = result.values().first().to<u64>();
|
||||
if (main_module_instance.memories().size() <= data.index.value()) {
|
||||
return InstantiationError {
|
||||
ByteString::formatted("Data segment referenced out-of-bounds memory ({}) of max {} entries",
|
||||
|
@ -570,5 +564,4 @@ void Linker::populate()
|
|||
m_unresolved_imports.set(m_ordered_imports.last());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -75,45 +75,57 @@ private:
|
|||
class Value {
|
||||
public:
|
||||
Value()
|
||||
: m_value(0)
|
||||
{
|
||||
}
|
||||
|
||||
using AnyValueType = Variant<i32, i64, float, double, u128, Reference>;
|
||||
explicit Value(AnyValueType value)
|
||||
: m_value(move(value))
|
||||
: m_value(u128())
|
||||
{
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
requires(sizeof(T) == sizeof(u64)) explicit Value(ValueType type, T raw_value)
|
||||
: m_value(0)
|
||||
requires(sizeof(T) == sizeof(u64)) explicit Value(T raw_value)
|
||||
: m_value(u128(bit_cast<i64>(raw_value), 0))
|
||||
{
|
||||
switch (type.kind()) {
|
||||
case ValueType::Kind::ExternReference:
|
||||
m_value = Reference { Reference::Extern { { bit_cast<u64>(raw_value) } } };
|
||||
break;
|
||||
case ValueType::Kind::FunctionReference:
|
||||
m_value = Reference { Reference::Func { { bit_cast<u64>(raw_value) } } };
|
||||
break;
|
||||
case ValueType::Kind::I32:
|
||||
m_value = static_cast<i32>(bit_cast<i64>(raw_value));
|
||||
break;
|
||||
case ValueType::Kind::I64:
|
||||
m_value = static_cast<i64>(bit_cast<u64>(raw_value));
|
||||
break;
|
||||
case ValueType::Kind::F32:
|
||||
m_value = static_cast<float>(bit_cast<double>(raw_value));
|
||||
break;
|
||||
case ValueType::Kind::F64:
|
||||
m_value = bit_cast<double>(raw_value);
|
||||
break;
|
||||
case ValueType::Kind::V128:
|
||||
m_value = u128(0ull, bit_cast<u64>(raw_value));
|
||||
break;
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
requires(sizeof(T) == sizeof(u32)) explicit Value(T raw_value)
|
||||
: m_value(u128(static_cast<i64>(bit_cast<i32>(raw_value)), 0))
|
||||
{
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
requires(sizeof(T) == sizeof(u8) && Signed<T>) explicit Value(T raw_value)
|
||||
: m_value(u128(static_cast<i64>(bit_cast<i8>(raw_value)), 0))
|
||||
{
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
requires(sizeof(T) == sizeof(u8) && Unsigned<T>) explicit Value(T raw_value)
|
||||
: m_value(u128(static_cast<u64>(bit_cast<u8>(raw_value)), 0))
|
||||
{
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
requires(sizeof(T) == sizeof(u16) && Signed<T>) explicit Value(T raw_value)
|
||||
: m_value(u128(static_cast<i64>(bit_cast<i16>(raw_value)), 0))
|
||||
{
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
requires(sizeof(T) == sizeof(u16) && Unsigned<T>) explicit Value(T raw_value)
|
||||
: m_value(u128(static_cast<u64>(bit_cast<u16>(raw_value)), 0))
|
||||
{
|
||||
}
|
||||
|
||||
explicit Value(Reference ref)
|
||||
{
|
||||
// Reference variant is encoded in the high storage of the u128:
|
||||
// 0: funcref
|
||||
// 1: externref
|
||||
// 2: null funcref
|
||||
// 3: null externref
|
||||
ref.ref().visit(
|
||||
[&](Reference::Func const& func) { m_value = u128(bit_cast<u64>(func.address), 0); },
|
||||
[&](Reference::Extern const& func) { m_value = u128(bit_cast<u64>(func.address), 1); },
|
||||
[&](Reference::Null const& null) { m_value = u128(0, null.type.kind() == ValueType::Kind::FunctionReference ? 2 : 3); });
|
||||
}
|
||||
|
||||
template<SameAs<u128> T>
|
||||
|
@ -128,63 +140,53 @@ public:
|
|||
ALWAYS_INLINE Value& operator=(Value const& value) = default;
|
||||
|
||||
template<typename T>
|
||||
ALWAYS_INLINE Optional<T> to() const
|
||||
ALWAYS_INLINE T to() const
|
||||
{
|
||||
Optional<T> result;
|
||||
m_value.visit(
|
||||
[&](auto value) {
|
||||
if constexpr (IsSame<T, decltype(value)> || (!IsFloatingPoint<T> && IsSame<decltype(value), MakeSigned<T>>)) {
|
||||
result = static_cast<T>(value);
|
||||
} else if constexpr (!IsFloatingPoint<T> && IsConvertible<decltype(value), T>) {
|
||||
// NOTE: No implicit vector <-> scalar conversion.
|
||||
if constexpr (!IsSame<T, u128>) {
|
||||
if (AK::is_within_range<T>(value))
|
||||
result = static_cast<T>(value);
|
||||
}
|
||||
}
|
||||
},
|
||||
[&](u128 value) {
|
||||
if constexpr (IsSame<T, u128>)
|
||||
result = value;
|
||||
},
|
||||
[&](Reference const& value) {
|
||||
if constexpr (IsSame<T, Reference>) {
|
||||
result = value;
|
||||
} else if constexpr (IsSame<T, Reference::Func>) {
|
||||
if (auto ptr = value.ref().template get_pointer<Reference::Func>())
|
||||
result = *ptr;
|
||||
} else if constexpr (IsSame<T, Reference::Extern>) {
|
||||
if (auto ptr = value.ref().template get_pointer<Reference::Extern>())
|
||||
result = *ptr;
|
||||
} else if constexpr (IsSame<T, Reference::Null>) {
|
||||
if (auto ptr = value.ref().template get_pointer<Reference::Null>())
|
||||
result = *ptr;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
if constexpr (IsSame<T, u128>) {
|
||||
return m_value;
|
||||
}
|
||||
if constexpr (IsSame<T, u32>) {
|
||||
u32 low = m_value.low() & 0xFFFFFFFF;
|
||||
return low;
|
||||
}
|
||||
if constexpr (IsSame<T, i32>) {
|
||||
u32 low = m_value.low() & 0xFFFFFFFF;
|
||||
return bit_cast<i32>(low);
|
||||
}
|
||||
if constexpr (IsSame<T, u64>) {
|
||||
return bit_cast<u64>(m_value.low());
|
||||
}
|
||||
if constexpr (IsSame<T, i64>) {
|
||||
return bit_cast<i64>(m_value.low());
|
||||
}
|
||||
if constexpr (IsSame<T, f32>) {
|
||||
u32 low = m_value.low() & 0xFFFFFFFF;
|
||||
return bit_cast<f32>(low);
|
||||
}
|
||||
if constexpr (IsSame<T, f64>) {
|
||||
return bit_cast<f64>(m_value.low());
|
||||
}
|
||||
if constexpr (IsSame<T, Reference>) {
|
||||
switch (m_value.high()) {
|
||||
case 0:
|
||||
return Reference { Reference::Func(bit_cast<FunctionAddress>(m_value.low())) };
|
||||
case 1:
|
||||
return Reference { Reference::Extern(bit_cast<ExternAddress>(m_value.low())) };
|
||||
case 2:
|
||||
return Reference { Reference::Null(ValueType(ValueType::Kind::FunctionReference)) };
|
||||
case 3:
|
||||
return Reference { Reference::Null(ValueType(ValueType::Kind::ExternReference)) };
|
||||
default:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
}
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
ValueType type() const
|
||||
{
|
||||
return ValueType(m_value.visit(
|
||||
[](i32) { return ValueType::Kind::I32; },
|
||||
[](i64) { return ValueType::Kind::I64; },
|
||||
[](float) { return ValueType::Kind::F32; },
|
||||
[](double) { return ValueType::Kind::F64; },
|
||||
[](u128) { return ValueType::Kind::V128; },
|
||||
[&](Reference const& type) {
|
||||
return type.ref().visit(
|
||||
[](Reference::Func const&) { return ValueType::Kind::FunctionReference; },
|
||||
[](Reference::Null const& null_type) {
|
||||
return null_type.type.kind();
|
||||
},
|
||||
[](Reference::Extern const&) { return ValueType::Kind::ExternReference; });
|
||||
}));
|
||||
}
|
||||
auto& value() const { return m_value; }
|
||||
|
||||
private:
|
||||
AnyValueType m_value;
|
||||
u128 m_value;
|
||||
};
|
||||
|
||||
struct Trap {
|
||||
|
@ -481,15 +483,16 @@ private:
|
|||
|
||||
class GlobalInstance {
|
||||
public:
|
||||
explicit GlobalInstance(Value value, bool is_mutable)
|
||||
explicit GlobalInstance(Value value, bool is_mutable, ValueType type)
|
||||
: m_mutable(is_mutable)
|
||||
, m_value(move(value))
|
||||
, m_value(value)
|
||||
, m_type(type)
|
||||
{
|
||||
}
|
||||
|
||||
auto is_mutable() const { return m_mutable; }
|
||||
auto& value() const { return m_value; }
|
||||
GlobalType type() const { return { m_value.type(), is_mutable() }; }
|
||||
GlobalType type() const { return { m_type, is_mutable() }; }
|
||||
void set_value(Value value)
|
||||
{
|
||||
VERIFY(is_mutable());
|
||||
|
@ -499,6 +502,7 @@ public:
|
|||
private:
|
||||
bool m_mutable { false };
|
||||
Value m_value;
|
||||
ValueType m_type;
|
||||
};
|
||||
|
||||
class DataInstance {
|
||||
|
|
|
@ -75,7 +75,7 @@ void BytecodeInterpreter::load_and_push(Configuration& configuration, Instructio
|
|||
auto& address = configuration.frame().module().memories()[arg.memory_index.value()];
|
||||
auto memory = configuration.store().get(address);
|
||||
auto& entry = configuration.value_stack().last();
|
||||
auto base = *entry.to<i32>();
|
||||
auto base = entry.to<i32>();
|
||||
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
|
||||
if (instance_address + sizeof(ReadType) > memory->size()) {
|
||||
m_trap = Trap { "Memory access out of bounds" };
|
||||
|
@ -100,7 +100,7 @@ void BytecodeInterpreter::load_and_push_mxn(Configuration& configuration, Instru
|
|||
auto& address = configuration.frame().module().memories()[arg.memory_index.value()];
|
||||
auto memory = configuration.store().get(address);
|
||||
auto& entry = configuration.value_stack().last();
|
||||
auto base = *entry.to<i32>();
|
||||
auto base = entry.to<i32>();
|
||||
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
|
||||
if (instance_address + M * N / 8 > memory->size()) {
|
||||
m_trap = Trap { "Memory access out of bounds" };
|
||||
|
@ -127,8 +127,8 @@ void BytecodeInterpreter::load_and_push_lane_n(Configuration& configuration, Ins
|
|||
auto memarg_and_lane = instruction.arguments().get<Instruction::MemoryAndLaneArgument>();
|
||||
auto& address = configuration.frame().module().memories()[memarg_and_lane.memory.memory_index.value()];
|
||||
auto memory = configuration.store().get(address);
|
||||
auto vector = *configuration.value_stack().take_last().to<u128>();
|
||||
auto base = *configuration.value_stack().take_last().to<u32>();
|
||||
auto vector = configuration.value_stack().take_last().to<u128>();
|
||||
auto base = configuration.value_stack().take_last().to<u32>();
|
||||
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + memarg_and_lane.memory.offset;
|
||||
if (instance_address + N / 8 > memory->size()) {
|
||||
m_trap = Trap { "Memory access out of bounds" };
|
||||
|
@ -146,7 +146,7 @@ void BytecodeInterpreter::load_and_push_zero_n(Configuration& configuration, Ins
|
|||
auto memarg_and_lane = instruction.arguments().get<Instruction::MemoryArgument>();
|
||||
auto& address = configuration.frame().module().memories()[memarg_and_lane.memory_index.value()];
|
||||
auto memory = configuration.store().get(address);
|
||||
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;
|
||||
if (instance_address + N / 8 > memory->size()) {
|
||||
m_trap = Trap { "Memory access out of bounds" };
|
||||
|
@ -165,7 +165,7 @@ void BytecodeInterpreter::load_and_push_m_splat(Configuration& configuration, In
|
|||
auto& address = configuration.frame().module().memories()[arg.memory_index.value()];
|
||||
auto memory = configuration.store().get(address);
|
||||
auto& entry = configuration.value_stack().last();
|
||||
auto base = *entry.to<i32>();
|
||||
auto base = entry.to<i32>();
|
||||
u64 instance_address = static_cast<u64>(bit_cast<u32>(base)) + arg.offset;
|
||||
if (instance_address + M / 8 > memory->size()) {
|
||||
m_trap = Trap { "Memory access out of bounds" };
|
||||
|
@ -212,7 +212,7 @@ void BytecodeInterpreter::pop_and_push_m_splat(Wasm::Configuration& configuratio
|
|||
using PopT = Conditional<M <= 32, NativeType<32>, NativeType<64>>;
|
||||
using ReadT = NativeType<M>;
|
||||
auto entry = configuration.value_stack().last();
|
||||
auto value = static_cast<ReadT>(*entry.to<PopT>());
|
||||
auto value = static_cast<ReadT>(entry.to<PopT>());
|
||||
dbgln_if(WASM_TRACE_DEBUG, "stack({}) -> splat({})", value, M);
|
||||
set_top_m_splat<M, NativeType>(configuration, value);
|
||||
}
|
||||
|
@ -220,7 +220,7 @@ void BytecodeInterpreter::pop_and_push_m_splat(Wasm::Configuration& configuratio
|
|||
template<typename M, template<typename> typename SetSign, typename VectorType>
|
||||
VectorType BytecodeInterpreter::pop_vector(Configuration& configuration)
|
||||
{
|
||||
return bit_cast<VectorType>(*configuration.value_stack().take_last().to<u128>());
|
||||
return bit_cast<VectorType>(configuration.value_stack().take_last().to<u128>());
|
||||
}
|
||||
|
||||
void BytecodeInterpreter::call_address(Configuration& configuration, FunctionAddress address)
|
||||
|
@ -268,7 +268,7 @@ void BytecodeInterpreter::binary_numeric_operation(Configuration& configuration,
|
|||
auto& lhs_entry = configuration.value_stack().last();
|
||||
auto lhs = lhs_entry.to<PopTypeLHS>();
|
||||
PushType result;
|
||||
auto call_result = Operator { forward<Args>(args)... }(lhs.value(), rhs.value());
|
||||
auto call_result = Operator { forward<Args>(args)... }(lhs, rhs);
|
||||
if constexpr (IsSpecializationOf<decltype(call_result), AK::ErrorOr>) {
|
||||
if (call_result.is_error()) {
|
||||
trap_if_not(false, call_result.error());
|
||||
|
@ -287,7 +287,7 @@ void BytecodeInterpreter::unary_operation(Configuration& configuration, Args&&..
|
|||
{
|
||||
auto& entry = configuration.value_stack().last();
|
||||
auto value = entry.to<PopType>();
|
||||
auto call_result = Operator { forward<Args>(args)... }(*value);
|
||||
auto call_result = Operator { forward<Args>(args)... }(value);
|
||||
PushType result;
|
||||
if constexpr (IsSpecializationOf<decltype(call_result), AK::ErrorOr>) {
|
||||
if (call_result.is_error()) {
|
||||
|
@ -337,19 +337,19 @@ void BytecodeInterpreter::pop_and_store(Configuration& configuration, Instructio
|
|||
{
|
||||
auto& memarg = instruction.arguments().get<Instruction::MemoryArgument>();
|
||||
auto entry = configuration.value_stack().take_last();
|
||||
auto value = ConvertToRaw<StoreT> {}(*entry.to<PopT>());
|
||||
auto value = ConvertToRaw<StoreT> {}(entry.to<PopT>());
|
||||
dbgln_if(WASM_TRACE_DEBUG, "stack({}) -> temporary({}b)", value, sizeof(StoreT));
|
||||
auto base = configuration.value_stack().take_last().to<i32>();
|
||||
store_to_memory(configuration, memarg, { &value, sizeof(StoreT) }, *base);
|
||||
store_to_memory(configuration, memarg, { &value, sizeof(StoreT) }, base);
|
||||
}
|
||||
|
||||
template<size_t N>
|
||||
void BytecodeInterpreter::pop_and_store_lane_n(Configuration& configuration, Instruction const& instruction)
|
||||
{
|
||||
auto& memarg_and_lane = instruction.arguments().get<Instruction::MemoryAndLaneArgument>();
|
||||
auto vector = *configuration.value_stack().take_last().to<u128>();
|
||||
auto vector = configuration.value_stack().take_last().to<u128>();
|
||||
auto src = bit_cast<u8*>(&vector) + memarg_and_lane.lane * N / 8;
|
||||
auto base = *configuration.value_stack().take_last().to<u32>();
|
||||
auto base = configuration.value_stack().take_last().to<u32>();
|
||||
store_to_memory(configuration, memarg_and_lane.memory, { src, N / 8 }, base);
|
||||
}
|
||||
|
||||
|
@ -422,16 +422,16 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
return;
|
||||
}
|
||||
case Instructions::i32_const.value():
|
||||
configuration.value_stack().append(Value(ValueType { ValueType::I32 }, static_cast<i64>(instruction.arguments().get<i32>())));
|
||||
configuration.value_stack().append(Value(instruction.arguments().get<i32>()));
|
||||
return;
|
||||
case Instructions::i64_const.value():
|
||||
configuration.value_stack().append(Value(ValueType { ValueType::I64 }, instruction.arguments().get<i64>()));
|
||||
configuration.value_stack().append(Value(instruction.arguments().get<i64>()));
|
||||
return;
|
||||
case Instructions::f32_const.value():
|
||||
configuration.value_stack().append(Value(Value::AnyValueType(instruction.arguments().get<float>())));
|
||||
configuration.value_stack().append(Value(instruction.arguments().get<float>()));
|
||||
return;
|
||||
case Instructions::f64_const.value():
|
||||
configuration.value_stack().append(Value(Value::AnyValueType(instruction.arguments().get<double>())));
|
||||
configuration.value_stack().append(Value(instruction.arguments().get<double>()));
|
||||
return;
|
||||
case Instructions::block.value(): {
|
||||
size_t arity = 0;
|
||||
|
@ -513,13 +513,13 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
return branch_to_label(configuration, instruction.arguments().get<LabelIndex>());
|
||||
case Instructions::br_if.value(): {
|
||||
auto cond = configuration.value_stack().take_last().to<i32>();
|
||||
if (*cond == 0)
|
||||
if (cond == 0)
|
||||
return;
|
||||
return branch_to_label(configuration, instruction.arguments().get<LabelIndex>());
|
||||
}
|
||||
case Instructions::br_table.value(): {
|
||||
auto& arguments = instruction.arguments().get<Instruction::TableBranchArgs>();
|
||||
auto i = *configuration.value_stack().take_last().to<u32>();
|
||||
auto i = configuration.value_stack().take_last().to<u32>();
|
||||
|
||||
if (i >= arguments.labels.size()) {
|
||||
return branch_to_label(configuration, arguments.default_);
|
||||
|
@ -538,12 +538,12 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
auto table_address = configuration.frame().module().tables()[args.table.value()];
|
||||
auto table_instance = configuration.store().get(table_address);
|
||||
auto index = configuration.value_stack().take_last().to<i32>();
|
||||
TRAP_IF_NOT(index.value() >= 0);
|
||||
TRAP_IF_NOT(static_cast<size_t>(index.value()) < table_instance->elements().size());
|
||||
auto element = table_instance->elements()[index.value()];
|
||||
TRAP_IF_NOT(index >= 0);
|
||||
TRAP_IF_NOT(static_cast<size_t>(index) < table_instance->elements().size());
|
||||
auto element = table_instance->elements()[index];
|
||||
TRAP_IF_NOT(element.ref().has<Reference::Func>());
|
||||
auto address = element.ref().get<Reference::Func>().address;
|
||||
dbgln_if(WASM_TRACE_DEBUG, "call_indirect({} -> {})", index.value(), address.value());
|
||||
dbgln_if(WASM_TRACE_DEBUG, "call_indirect({} -> {})", index, address.value());
|
||||
call_address(configuration, address);
|
||||
return;
|
||||
}
|
||||
|
@ -636,8 +636,8 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
i32 old_pages = instance->size() / Constants::page_size;
|
||||
auto& entry = configuration.value_stack().last();
|
||||
auto new_pages = entry.to<i32>();
|
||||
dbgln_if(WASM_TRACE_DEBUG, "memory.grow({}), previously {} pages...", *new_pages, old_pages);
|
||||
if (instance->grow(new_pages.value() * Constants::page_size))
|
||||
dbgln_if(WASM_TRACE_DEBUG, "memory.grow({}), previously {} pages...", new_pages, old_pages);
|
||||
if (instance->grow(new_pages * Constants::page_size))
|
||||
entry = Value((i32)old_pages);
|
||||
else
|
||||
entry = Value((i32)-1);
|
||||
|
@ -648,9 +648,9 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
auto& args = instruction.arguments().get<Instruction::MemoryIndexArgument>();
|
||||
auto address = configuration.frame().module().memories()[args.memory_index.value()];
|
||||
auto instance = configuration.store().get(address);
|
||||
auto count = configuration.value_stack().take_last().to<u32>().value();
|
||||
u8 value = static_cast<u8>(configuration.value_stack().take_last().to<u32>().value());
|
||||
auto destination_offset = configuration.value_stack().take_last().to<u32>().value();
|
||||
auto count = configuration.value_stack().take_last().to<u32>();
|
||||
u8 value = static_cast<u8>(configuration.value_stack().take_last().to<u32>());
|
||||
auto destination_offset = configuration.value_stack().take_last().to<u32>();
|
||||
|
||||
TRAP_IF_NOT(static_cast<size_t>(destination_offset + count) <= instance->data().size());
|
||||
|
||||
|
@ -670,9 +670,9 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
auto source_instance = configuration.store().get(source_address);
|
||||
auto destination_instance = configuration.store().get(destination_address);
|
||||
|
||||
auto count = configuration.value_stack().take_last().to<i32>().value();
|
||||
auto source_offset = configuration.value_stack().take_last().to<i32>().value();
|
||||
auto destination_offset = configuration.value_stack().take_last().to<i32>().value();
|
||||
auto count = configuration.value_stack().take_last().to<i32>();
|
||||
auto source_offset = configuration.value_stack().take_last().to<i32>();
|
||||
auto destination_offset = configuration.value_stack().take_last().to<i32>();
|
||||
|
||||
Checked<size_t> source_position = source_offset;
|
||||
source_position.saturating_add(count);
|
||||
|
@ -706,9 +706,9 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
auto& data = *configuration.store().get(data_address);
|
||||
auto memory_address = configuration.frame().module().memories()[args.memory_index.value()];
|
||||
auto memory = configuration.store().get(memory_address);
|
||||
auto count = *configuration.value_stack().take_last().to<u32>();
|
||||
auto source_offset = *configuration.value_stack().take_last().to<u32>();
|
||||
auto destination_offset = *configuration.value_stack().take_last().to<u32>();
|
||||
auto count = configuration.value_stack().take_last().to<u32>();
|
||||
auto source_offset = configuration.value_stack().take_last().to<u32>();
|
||||
auto destination_offset = configuration.value_stack().take_last().to<u32>();
|
||||
|
||||
Checked<size_t> source_position = source_offset;
|
||||
source_position.saturating_add(count);
|
||||
|
@ -747,9 +747,9 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
auto table = configuration.store().get(table_address);
|
||||
auto element_address = configuration.frame().module().elements()[args.element_index.value()];
|
||||
auto element = configuration.store().get(element_address);
|
||||
auto count = *configuration.value_stack().take_last().to<u32>();
|
||||
auto source_offset = *configuration.value_stack().take_last().to<u32>();
|
||||
auto destination_offset = *configuration.value_stack().take_last().to<u32>();
|
||||
auto count = configuration.value_stack().take_last().to<u32>();
|
||||
auto source_offset = configuration.value_stack().take_last().to<u32>();
|
||||
auto destination_offset = configuration.value_stack().take_last().to<u32>();
|
||||
|
||||
Checked<u32> checked_source_offset = source_offset;
|
||||
Checked<u32> checked_destination_offset = destination_offset;
|
||||
|
@ -769,9 +769,9 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
auto source_instance = configuration.store().get(source_address);
|
||||
auto destination_instance = configuration.store().get(destination_address);
|
||||
|
||||
auto count = configuration.value_stack().take_last().to<u32>().value();
|
||||
auto source_offset = configuration.value_stack().take_last().to<u32>().value();
|
||||
auto destination_offset = configuration.value_stack().take_last().to<u32>().value();
|
||||
auto count = configuration.value_stack().take_last().to<u32>();
|
||||
auto source_offset = configuration.value_stack().take_last().to<u32>();
|
||||
auto destination_offset = configuration.value_stack().take_last().to<u32>();
|
||||
|
||||
Checked<size_t> source_position = source_offset;
|
||||
source_position.saturating_add(count);
|
||||
|
@ -801,9 +801,9 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
auto table_index = instruction.arguments().get<TableIndex>();
|
||||
auto address = configuration.frame().module().tables()[table_index.value()];
|
||||
auto table = configuration.store().get(address);
|
||||
auto count = *configuration.value_stack().take_last().to<u32>();
|
||||
auto value = *configuration.value_stack().take_last().to<Reference>();
|
||||
auto start = *configuration.value_stack().take_last().to<u32>();
|
||||
auto count = configuration.value_stack().take_last().to<u32>();
|
||||
auto value = configuration.value_stack().take_last().to<Reference>();
|
||||
auto start = configuration.value_stack().take_last().to<u32>();
|
||||
|
||||
Checked<u32> checked_offset = start;
|
||||
checked_offset += count;
|
||||
|
@ -814,8 +814,8 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
return;
|
||||
}
|
||||
case Instructions::table_set.value(): {
|
||||
auto ref = *configuration.value_stack().take_last().to<Reference>();
|
||||
auto index = (size_t)(*configuration.value_stack().take_last().to<i32>());
|
||||
auto ref = configuration.value_stack().take_last().to<Reference>();
|
||||
auto index = (size_t)(configuration.value_stack().take_last().to<i32>());
|
||||
auto table_index = instruction.arguments().get<TableIndex>();
|
||||
auto address = configuration.frame().module().tables()[table_index.value()];
|
||||
auto table = configuration.store().get(address);
|
||||
|
@ -824,7 +824,7 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
return;
|
||||
}
|
||||
case Instructions::table_get.value(): {
|
||||
auto index = (size_t)(*configuration.value_stack().take_last().to<i32>());
|
||||
auto index = (size_t)(configuration.value_stack().take_last().to<i32>());
|
||||
auto table_index = instruction.arguments().get<TableIndex>();
|
||||
auto address = configuration.frame().module().tables()[table_index.value()];
|
||||
auto table = configuration.store().get(address);
|
||||
|
@ -834,8 +834,8 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
return;
|
||||
}
|
||||
case Instructions::table_grow.value(): {
|
||||
auto size = *configuration.value_stack().take_last().to<u32>();
|
||||
auto fill_value = *configuration.value_stack().take_last().to<Reference>();
|
||||
auto size = configuration.value_stack().take_last().to<u32>();
|
||||
auto fill_value = configuration.value_stack().take_last().to<Reference>();
|
||||
auto table_index = instruction.arguments().get<TableIndex>();
|
||||
auto address = configuration.frame().module().tables()[table_index.value()];
|
||||
auto table = configuration.store().get(address);
|
||||
|
@ -864,12 +864,12 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
auto index = instruction.arguments().get<FunctionIndex>().value();
|
||||
auto& functions = configuration.frame().module().functions();
|
||||
auto address = functions[index];
|
||||
configuration.value_stack().append(Value(ValueType(ValueType::FunctionReference), address.value()));
|
||||
configuration.value_stack().append(Value(address.value()));
|
||||
return;
|
||||
}
|
||||
case Instructions::ref_is_null.value(): {
|
||||
auto ref = configuration.value_stack().take_last().to<Reference::Null>();
|
||||
configuration.value_stack().append(Value(static_cast<i32>(ref.has_value() ? 1 : 0)));
|
||||
auto ref = configuration.value_stack().take_last().to<Reference>();
|
||||
configuration.value_stack().append(Value(static_cast<i32>(ref.ref().has<Reference::Null>() ? 1 : 0)));
|
||||
return;
|
||||
}
|
||||
case Instructions::drop.value():
|
||||
|
@ -879,10 +879,10 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
case Instructions::select_typed.value(): {
|
||||
// Note: The type seems to only be used for validation.
|
||||
auto value = configuration.value_stack().take_last().to<i32>();
|
||||
dbgln_if(WASM_TRACE_DEBUG, "select({})", value.value());
|
||||
dbgln_if(WASM_TRACE_DEBUG, "select({})", value);
|
||||
auto rhs = configuration.value_stack().take_last();
|
||||
auto& lhs = configuration.value_stack().last();
|
||||
lhs = value.value() != 0 ? lhs : rhs;
|
||||
lhs = value != 0 ? lhs : rhs;
|
||||
return;
|
||||
}
|
||||
case Instructions::i32_eqz.value():
|
||||
|
@ -1579,15 +1579,15 @@ ALWAYS_INLINE void BytecodeInterpreter::interpret_instruction(Configuration& con
|
|||
case Instructions::v128_andnot.value():
|
||||
return binary_numeric_operation<u128, u128, Operators::BitAndNot>(configuration);
|
||||
case Instructions::v128_bitselect.value(): {
|
||||
auto mask = *configuration.value_stack().take_last().to<u128>();
|
||||
auto false_vector = *configuration.value_stack().take_last().to<u128>();
|
||||
auto true_vector = *configuration.value_stack().take_last().to<u128>();
|
||||
auto mask = configuration.value_stack().take_last().to<u128>();
|
||||
auto false_vector = configuration.value_stack().take_last().to<u128>();
|
||||
auto true_vector = configuration.value_stack().take_last().to<u128>();
|
||||
u128 result = (true_vector & mask) | (false_vector & ~mask);
|
||||
configuration.value_stack().append(Value(result));
|
||||
return;
|
||||
}
|
||||
case Instructions::v128_any_true.value(): {
|
||||
auto vector = *configuration.value_stack().take_last().to<u128>();
|
||||
auto vector = configuration.value_stack().take_last().to<u128>();
|
||||
configuration.value_stack().append(Value(static_cast<i32>(vector != 0)));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ Result Configuration::call(Interpreter& interpreter, FunctionAddress address, Ve
|
|||
locals.ensure_capacity(locals.size() + wasm_function->code().func().locals().size());
|
||||
for (auto& local : wasm_function->code().func().locals()) {
|
||||
for (size_t i = 0; i < local.n(); ++i)
|
||||
locals.empend(local.type(), 0ull);
|
||||
locals.append(Value());
|
||||
}
|
||||
|
||||
set_frame(Frame {
|
||||
|
|
|
@ -654,24 +654,41 @@ void Printer::print(Wasm::ValueType const& type)
|
|||
print("(type {})\n", ValueType::kind_name(type.kind()));
|
||||
}
|
||||
|
||||
void Printer::print(Wasm::Value const& value, Wasm::ValueType const& type)
|
||||
{
|
||||
print_indent();
|
||||
switch (type.kind()) {
|
||||
case ValueType::I32:
|
||||
print(ByteString::formatted("{}", value.to<i32>()));
|
||||
break;
|
||||
case ValueType::I64:
|
||||
print(ByteString::formatted("{}", value.to<i64>()));
|
||||
break;
|
||||
case ValueType::F32:
|
||||
print(ByteString::formatted("{}", value.to<f32>()));
|
||||
break;
|
||||
case ValueType::F64:
|
||||
print(ByteString::formatted("{}", value.to<f64>()));
|
||||
break;
|
||||
case ValueType::V128:
|
||||
print(ByteString::formatted("v128({:x})", value.value()));
|
||||
break;
|
||||
case ValueType::FunctionReference:
|
||||
case ValueType::ExternReference:
|
||||
print(ByteString::formatted("addr({})",
|
||||
value.to<Reference>().ref().visit(
|
||||
[](Wasm::Reference::Null const&) { return ByteString("null"); },
|
||||
[](auto const& ref) { return ByteString::number(ref.address.value()); })));
|
||||
break;
|
||||
}
|
||||
TemporaryChange<size_t> change { m_indent, 0 };
|
||||
}
|
||||
|
||||
void Printer::print(Wasm::Value const& value)
|
||||
{
|
||||
print_indent();
|
||||
print("{} ", value.value().visit([&]<typename T>(T const& value) {
|
||||
if constexpr (IsSame<Wasm::Reference, T>) {
|
||||
return ByteString::formatted(
|
||||
"addr({})",
|
||||
value.ref().visit(
|
||||
[](Wasm::Reference::Null const&) { return ByteString("null"); },
|
||||
[](auto const& ref) { return ByteString::number(ref.address.value()); }));
|
||||
} else if constexpr (IsSame<u128, T>) {
|
||||
return ByteString::formatted("v128({:x})", value);
|
||||
} else {
|
||||
return ByteString::formatted("{}", value);
|
||||
}
|
||||
}));
|
||||
print("{:x}", value.value());
|
||||
TemporaryChange<size_t> change { m_indent, 0 };
|
||||
print(value.type());
|
||||
}
|
||||
|
||||
void Printer::print(Wasm::Reference const& value)
|
||||
|
|
|
@ -59,6 +59,7 @@ struct Printer {
|
|||
void print(Wasm::TypeSection const&);
|
||||
void print(Wasm::ValueType const&);
|
||||
void print(Wasm::Value const&);
|
||||
void print(Wasm::Value const&, ValueType const&);
|
||||
|
||||
private:
|
||||
void print_indent();
|
||||
|
|
|
@ -68,7 +68,7 @@ CompatibleValue<T> to_compatible_value(Wasm::Value const& value)
|
|||
{
|
||||
using Type = typename ToCompatibleValue<T>::Type;
|
||||
// Note: the type can't be something else, we've already checked before through the function type's runtime checker.
|
||||
auto converted_value = *value.template to<Type>();
|
||||
auto converted_value = value.template to<Type>();
|
||||
return { .value = converted_value };
|
||||
}
|
||||
|
||||
|
@ -868,7 +868,7 @@ static Array<Bytes, N> address_spans(Span<Value> values, Configuration& configur
|
|||
Array<Bytes, N> result;
|
||||
auto memory = configuration.store().get(MemoryAddress { 0 })->data().span();
|
||||
for (size_t i = 0; i < N; ++i)
|
||||
result[i] = memory.slice(*values[i].to<i32>());
|
||||
result[i] = memory.slice(values[i].to<i32>());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -1027,7 +1027,7 @@ struct InvocationOf<impl> {
|
|||
auto value = result.release_value();
|
||||
if constexpr (IsSpecializationOf<RV, Result>) {
|
||||
if (value.is_error())
|
||||
return Wasm::Result { Vector { Value { ValueType(ValueType::I32), static_cast<u64>(to_underlying(value.error().value())) } } };
|
||||
return Wasm::Result { Vector { Value { static_cast<u32>(to_underlying(value.error().value())) } } };
|
||||
}
|
||||
|
||||
if constexpr (!IsVoid<R>) {
|
||||
|
@ -1040,7 +1040,7 @@ struct InvocationOf<impl> {
|
|||
}
|
||||
}
|
||||
// Return value is errno, we have nothing to return.
|
||||
return Wasm::Result { Vector<Value> { Value(ValueType(ValueType::I32), 0ull) } };
|
||||
return Wasm::Result { Vector<Value> { Value() } };
|
||||
},
|
||||
FunctionType {
|
||||
move(arguments_types),
|
||||
|
@ -1227,20 +1227,6 @@ FDFlags fd_flags_of(struct stat const&)
|
|||
}
|
||||
|
||||
namespace AK {
|
||||
template<>
|
||||
struct Formatter<Wasm::Value> : AK::Formatter<FormatString> {
|
||||
ErrorOr<void> format(FormatBuilder& builder, Wasm::Value const& value)
|
||||
{
|
||||
return value.value().visit(
|
||||
[&](Wasm::Reference const&) {
|
||||
return Formatter<FormatString>::format(builder, "({}) &r"sv, Wasm::ValueType::kind_name(value.type().kind()));
|
||||
},
|
||||
[&](auto const& v) {
|
||||
return Formatter<FormatString>::format(builder, "({}) {}"sv, Wasm::ValueType::kind_name(value.type().kind()), v);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct Formatter<Wasm::Wasi::Errno> : AK::Formatter<FormatString> {
|
||||
ErrorOr<void> format(FormatBuilder& builder, Wasm::Wasi::Errno const& value)
|
||||
|
|
|
@ -32,7 +32,7 @@ static Wasm::ValueType table_kind_to_value_type(Bindings::TableKind kind)
|
|||
static JS::ThrowCompletionOr<Wasm::Value> value_to_reference(JS::VM& vm, JS::Value value, Wasm::ValueType const& reference_type)
|
||||
{
|
||||
if (value.is_undefined())
|
||||
return Wasm::Value(reference_type, 0ull);
|
||||
return Wasm::Value();
|
||||
return Detail::to_webassembly_value(vm, value, reference_type);
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ WebIDL::ExceptionOr<JS::NonnullGCPtr<Table>> Table::construct_impl(JS::Realm& re
|
|||
if (!address.has_value())
|
||||
return vm.throw_completion<JS::TypeError>("Wasm Table allocation failed"sv);
|
||||
|
||||
auto const& reference = reference_value.value().get<Wasm::Reference>();
|
||||
auto const& reference = reference_value.to<Wasm::Reference>();
|
||||
auto& table = *cache.abstract_machine().store().get(*address);
|
||||
for (auto& element : table.elements())
|
||||
element = reference;
|
||||
|
@ -84,7 +84,7 @@ WebIDL::ExceptionOr<u32> Table::grow(u32 delta, JS::Value value)
|
|||
auto initial_size = table->elements().size();
|
||||
|
||||
auto reference_value = TRY(value_to_reference(vm, value, table->type().element_type()));
|
||||
auto const& reference = reference_value.value().get<Wasm::Reference>();
|
||||
auto const& reference = reference_value.to<Wasm::Reference>();
|
||||
|
||||
if (!table->grow(delta, reference))
|
||||
return vm.throw_completion<JS::RangeError>("Failed to grow table"sv);
|
||||
|
@ -108,7 +108,7 @@ WebIDL::ExceptionOr<JS::Value> Table::get(u32 index) const
|
|||
auto& ref = table->elements()[index];
|
||||
|
||||
Wasm::Value wasm_value { ref };
|
||||
return Detail::to_js_value(vm, wasm_value);
|
||||
return Detail::to_js_value(vm, wasm_value, table->type().element_type());
|
||||
}
|
||||
|
||||
// https://webassembly.github.io/spec/js-api/#dom-table-set
|
||||
|
@ -125,7 +125,7 @@ WebIDL::ExceptionOr<void> Table::set(u32 index, JS::Value value)
|
|||
return vm.throw_completion<JS::RangeError>("Table element index out of range"sv);
|
||||
|
||||
auto reference_value = TRY(value_to_reference(vm, value, table->type().element_type()));
|
||||
auto const& reference = reference_value.value().get<Wasm::Reference>();
|
||||
auto const& reference = reference_value.to<Wasm::Reference>();
|
||||
|
||||
table->elements()[index] = reference;
|
||||
|
||||
|
|
|
@ -193,8 +193,11 @@ JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS
|
|||
Wasm::HostFunction host_function {
|
||||
[&](auto&, auto& arguments) -> Wasm::Result {
|
||||
JS::MarkedVector<JS::Value> argument_values { vm.heap() };
|
||||
for (auto& entry : arguments)
|
||||
argument_values.append(to_js_value(vm, entry));
|
||||
size_t index = 0;
|
||||
for (auto& entry : arguments) {
|
||||
argument_values.append(to_js_value(vm, entry, type.parameters()[index]));
|
||||
++index;
|
||||
}
|
||||
|
||||
auto result = TRY(JS::call(vm, function, JS::js_undefined(), argument_values.span()));
|
||||
if (type.results().is_empty())
|
||||
|
@ -378,11 +381,13 @@ JS::NativeFunction* create_native_function(JS::VM& vm, Wasm::FunctionAddress add
|
|||
return JS::js_undefined();
|
||||
|
||||
if (result.values().size() == 1)
|
||||
return to_js_value(vm, result.values().first());
|
||||
return to_js_value(vm, result.values().first(), type.results().first());
|
||||
|
||||
return JS::Value(JS::Array::create_from<Wasm::Value>(realm, result.values(), [&](Wasm::Value value) {
|
||||
return to_js_value(vm, value);
|
||||
}));
|
||||
VERIFY_NOT_REACHED();
|
||||
// TODO
|
||||
/*return JS::Value(JS::Array::create_from<Wasm::Value>(realm, result.values(), [&](Wasm::Value value) {*/
|
||||
/* return to_js_value(vm, value);*/
|
||||
/*}));*/
|
||||
});
|
||||
|
||||
cache.add_function_instance(address, function);
|
||||
|
@ -417,7 +422,7 @@ JS::ThrowCompletionOr<Wasm::Value> to_webassembly_value(JS::VM& vm, JS::Value va
|
|||
}
|
||||
case Wasm::ValueType::FunctionReference: {
|
||||
if (value.is_null())
|
||||
return Wasm::Value { Wasm::ValueType(Wasm::ValueType::FunctionReference), 0ull };
|
||||
return Wasm::Value();
|
||||
|
||||
if (value.is_function()) {
|
||||
auto& function = value.as_function();
|
||||
|
@ -439,20 +444,20 @@ JS::ThrowCompletionOr<Wasm::Value> to_webassembly_value(JS::VM& vm, JS::Value va
|
|||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
|
||||
JS::Value to_js_value(JS::VM& vm, Wasm::Value& wasm_value)
|
||||
JS::Value to_js_value(JS::VM& vm, Wasm::Value& wasm_value, Wasm::ValueType type)
|
||||
{
|
||||
auto& realm = *vm.current_realm();
|
||||
switch (wasm_value.type().kind()) {
|
||||
switch (type.kind()) {
|
||||
case Wasm::ValueType::I64:
|
||||
return realm.heap().allocate<JS::BigInt>(realm, ::Crypto::SignedBigInteger { wasm_value.to<i64>().value() });
|
||||
return realm.heap().allocate<JS::BigInt>(realm, ::Crypto::SignedBigInteger { wasm_value.to<i64>() });
|
||||
case Wasm::ValueType::I32:
|
||||
return JS::Value(wasm_value.to<i32>().value());
|
||||
return JS::Value(wasm_value.to<i32>());
|
||||
case Wasm::ValueType::F64:
|
||||
return JS::Value(wasm_value.to<double>().value());
|
||||
return JS::Value(wasm_value.to<double>());
|
||||
case Wasm::ValueType::F32:
|
||||
return JS::Value(static_cast<double>(wasm_value.to<float>().value()));
|
||||
return JS::Value(static_cast<double>(wasm_value.to<float>()));
|
||||
case Wasm::ValueType::FunctionReference: {
|
||||
auto ref_ = *wasm_value.to<Wasm::Reference>();
|
||||
auto ref_ = wasm_value.to<Wasm::Reference>();
|
||||
if (ref_.ref().has<Wasm::Reference::Null>())
|
||||
return JS::js_null();
|
||||
auto address = ref_.ref().get<Wasm::Reference::Func>().address;
|
||||
|
|
|
@ -62,7 +62,7 @@ JS::ThrowCompletionOr<NonnullOwnPtr<Wasm::ModuleInstance>> instantiate_module(JS
|
|||
JS::ThrowCompletionOr<NonnullRefPtr<CompiledWebAssemblyModule>> parse_module(JS::VM&, JS::Object* buffer);
|
||||
JS::NativeFunction* create_native_function(JS::VM&, Wasm::FunctionAddress address, ByteString const& name, Instance* instance = nullptr);
|
||||
JS::ThrowCompletionOr<Wasm::Value> to_webassembly_value(JS::VM&, JS::Value value, Wasm::ValueType const& type);
|
||||
JS::Value to_js_value(JS::VM&, Wasm::Value& wasm_value);
|
||||
JS::Value to_js_value(JS::VM&, Wasm::Value& wasm_value, Wasm::ValueType type);
|
||||
|
||||
extern HashMap<JS::GCPtr<JS::Object>, WebAssemblyCache> s_caches;
|
||||
|
||||
|
|
|
@ -31,6 +31,11 @@ static void (*old_signal)(int);
|
|||
static StackInfo g_stack_info;
|
||||
static Wasm::DebuggerBytecodeInterpreter g_interpreter(g_stack_info);
|
||||
|
||||
struct ParsedValue {
|
||||
Wasm::Value value;
|
||||
Wasm::ValueType type;
|
||||
};
|
||||
|
||||
static void sigint_handler(int)
|
||||
{
|
||||
if (!g_continue) {
|
||||
|
@ -81,7 +86,7 @@ static Optional<u128> convert_to_uint_from_hex(StringView string)
|
|||
return value;
|
||||
}
|
||||
|
||||
static ErrorOr<Wasm::Value> parse_value(StringView spec)
|
||||
static ErrorOr<ParsedValue> parse_value(StringView spec)
|
||||
{
|
||||
constexpr auto is_sep = [](char c) { return is_ascii_space(c) || c == ':'; };
|
||||
// Scalar: 'T.const[:\s]v' (i32.const 42)
|
||||
|
@ -127,42 +132,63 @@ static ErrorOr<Wasm::Value> parse_value(StringView spec)
|
|||
lexer.ignore_while(is_sep);
|
||||
// The rest of the string is the value
|
||||
auto text = lexer.consume_all();
|
||||
return parse_u128(text);
|
||||
return ParsedValue {
|
||||
.value = TRY(parse_u128(text)),
|
||||
.type = Wasm::ValueType(Wasm::ValueType::Kind::V128)
|
||||
};
|
||||
}
|
||||
|
||||
if (lexer.consume_specific("i8.const"sv)) {
|
||||
lexer.ignore_while(is_sep);
|
||||
auto text = lexer.consume_all();
|
||||
return parse_scalar.operator()<i8>(text);
|
||||
return ParsedValue {
|
||||
.value = TRY(parse_scalar.operator()<i8>(text)),
|
||||
.type = Wasm::ValueType(Wasm::ValueType::Kind::I32)
|
||||
};
|
||||
}
|
||||
if (lexer.consume_specific("i16.const"sv)) {
|
||||
lexer.ignore_while(is_sep);
|
||||
auto text = lexer.consume_all();
|
||||
return parse_scalar.operator()<i16>(text);
|
||||
return ParsedValue {
|
||||
.value = TRY(parse_scalar.operator()<i16>(text)),
|
||||
.type = Wasm::ValueType(Wasm::ValueType::Kind::I32)
|
||||
};
|
||||
}
|
||||
if (lexer.consume_specific("i32.const"sv)) {
|
||||
lexer.ignore_while(is_sep);
|
||||
auto text = lexer.consume_all();
|
||||
return parse_scalar.operator()<i32>(text);
|
||||
return ParsedValue {
|
||||
.value = TRY(parse_scalar.operator()<i32>(text)),
|
||||
.type = Wasm::ValueType(Wasm::ValueType::Kind::I32)
|
||||
};
|
||||
}
|
||||
if (lexer.consume_specific("i64.const"sv)) {
|
||||
lexer.ignore_while(is_sep);
|
||||
auto text = lexer.consume_all();
|
||||
return parse_scalar.operator()<i64>(text);
|
||||
return ParsedValue {
|
||||
.value = TRY(parse_scalar.operator()<i64>(text)),
|
||||
.type = Wasm::ValueType(Wasm::ValueType::Kind::I64)
|
||||
};
|
||||
}
|
||||
if (lexer.consume_specific("f32.const"sv)) {
|
||||
lexer.ignore_while(is_sep);
|
||||
auto text = lexer.consume_all();
|
||||
return parse_scalar.operator()<float>(text);
|
||||
return ParsedValue {
|
||||
.value = TRY(parse_scalar.operator()<float>(text)),
|
||||
.type = Wasm::ValueType(Wasm::ValueType::Kind::F32)
|
||||
};
|
||||
}
|
||||
if (lexer.consume_specific("f64.const"sv)) {
|
||||
lexer.ignore_while(is_sep);
|
||||
auto text = lexer.consume_all();
|
||||
return parse_scalar.operator()<double>(text);
|
||||
return ParsedValue {
|
||||
.value = TRY(parse_scalar.operator()<double>(text)),
|
||||
.type = Wasm::ValueType(Wasm::ValueType::Kind::F64)
|
||||
};
|
||||
}
|
||||
|
||||
if (lexer.consume_specific("v("sv)) {
|
||||
Vector<Wasm::Value> values;
|
||||
Vector<ParsedValue> values;
|
||||
for (;;) {
|
||||
lexer.ignore_while(is_sep);
|
||||
if (lexer.consume_specific(")"sv))
|
||||
|
@ -181,9 +207,9 @@ static ErrorOr<Wasm::Value> parse_value(StringView spec)
|
|||
if (values.is_empty())
|
||||
return Error::from_string_literal("Empty vector");
|
||||
|
||||
auto element_type = values.first().type();
|
||||
auto element_type = values.first().type;
|
||||
for (auto& value : values) {
|
||||
if (value.type() != element_type)
|
||||
if (value.type != element_type)
|
||||
return Error::from_string_literal("Mixed types in vector");
|
||||
}
|
||||
|
||||
|
@ -191,24 +217,25 @@ static ErrorOr<Wasm::Value> parse_value(StringView spec)
|
|||
unsigned width = 0;
|
||||
u128 result = 0;
|
||||
u128 last_value = 0;
|
||||
for (auto& value : values) {
|
||||
for (auto& parsed : values) {
|
||||
if (total_size >= 128)
|
||||
return Error::from_string_literal("Vector too large");
|
||||
|
||||
width = value.value().visit(
|
||||
[&](Integral auto x) {
|
||||
last_value = u128(x, 0);
|
||||
return sizeof(x);
|
||||
},
|
||||
[&](float x) {
|
||||
last_value = u128(bit_cast<u32>(x), 0);
|
||||
return sizeof(x);
|
||||
},
|
||||
[&](double x) {
|
||||
last_value = u128(bit_cast<u64>(x), 0);
|
||||
return sizeof(x);
|
||||
},
|
||||
[&](auto) -> size_t { VERIFY_NOT_REACHED(); });
|
||||
switch (parsed.type.kind()) {
|
||||
case Wasm::ValueType::F32:
|
||||
case Wasm::ValueType::I32:
|
||||
width = sizeof(u32);
|
||||
break;
|
||||
case Wasm::ValueType::F64:
|
||||
case Wasm::ValueType::I64:
|
||||
width = sizeof(u64);
|
||||
break;
|
||||
case Wasm::ValueType::V128:
|
||||
case Wasm::ValueType::FunctionReference:
|
||||
case Wasm::ValueType::ExternReference:
|
||||
VERIFY_NOT_REACHED();
|
||||
}
|
||||
last_value = parsed.value.value();
|
||||
|
||||
result |= last_value << total_size;
|
||||
total_size += width * 8;
|
||||
|
@ -222,7 +249,10 @@ static ErrorOr<Wasm::Value> parse_value(StringView spec)
|
|||
total_size += width * 8;
|
||||
}
|
||||
|
||||
return Wasm::Value { result };
|
||||
return ParsedValue {
|
||||
.value = Wasm::Value { result },
|
||||
.type = Wasm::ValueType(Wasm::ValueType::Kind::V128)
|
||||
};
|
||||
}
|
||||
|
||||
return Error::from_string_literal("Invalid value");
|
||||
|
@ -390,7 +420,7 @@ static bool pre_interpret_hook(Wasm::Configuration& config, Wasm::InstructionPoi
|
|||
warnln("Expected {} arguments for call, but found only {}", type.parameters().size(), args.size() - 2);
|
||||
continue;
|
||||
}
|
||||
Vector<Wasm::Value> values_to_push;
|
||||
Vector<ParsedValue> values_to_push;
|
||||
Vector<Wasm::Value> values;
|
||||
auto ok = true;
|
||||
for (size_t index = 2; index < args.size(); ++index) {
|
||||
|
@ -406,12 +436,12 @@ static bool pre_interpret_hook(Wasm::Configuration& config, Wasm::InstructionPoi
|
|||
continue;
|
||||
for (auto& param : type.parameters()) {
|
||||
auto v = values_to_push.take_last();
|
||||
if (v.type() != param) {
|
||||
warnln("Type mismatch in argument: expected {}, but got {}", Wasm::ValueType::kind_name(param.kind()), Wasm::ValueType::kind_name(v.type().kind()));
|
||||
if (v.type != param) {
|
||||
warnln("Type mismatch in argument: expected {}, but got {}", Wasm::ValueType::kind_name(param.kind()), Wasm::ValueType::kind_name(v.type.kind()));
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
values.append(v);
|
||||
values.append(v.value);
|
||||
}
|
||||
if (!ok)
|
||||
continue;
|
||||
|
@ -426,9 +456,11 @@ static bool pre_interpret_hook(Wasm::Configuration& config, Wasm::InstructionPoi
|
|||
} else {
|
||||
if (!result.values().is_empty())
|
||||
warnln("Returned:");
|
||||
size_t index = 0;
|
||||
for (auto& value : result.values()) {
|
||||
g_stdout->write_until_depleted(" -> "sv.bytes()).release_value_but_fixme_should_propagate_errors();
|
||||
g_printer->print(value);
|
||||
g_printer->print(value, type.results()[index]);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
|
@ -492,7 +524,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
|||
bool shell_mode = false;
|
||||
bool wasi = false;
|
||||
ByteString exported_function_to_execute;
|
||||
Vector<Wasm::Value> values_to_push;
|
||||
Vector<ParsedValue> values_to_push;
|
||||
Vector<ByteString> modules_to_link_in;
|
||||
Vector<StringView> args_if_wasi;
|
||||
Vector<StringView> wasi_preopened_mappings;
|
||||
|
@ -677,9 +709,11 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
|||
[name = entry.name, type = type](auto&, auto& arguments) -> Wasm::Result {
|
||||
StringBuilder argument_builder;
|
||||
bool first = true;
|
||||
size_t index = 0;
|
||||
for (auto& argument : arguments) {
|
||||
AllocatingMemoryStream stream;
|
||||
Wasm::Printer { stream }.print(argument);
|
||||
auto value_type = type.parameters()[index];
|
||||
Wasm::Printer { stream }.print(argument, value_type);
|
||||
if (first)
|
||||
first = false;
|
||||
else
|
||||
|
@ -687,12 +721,13 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
|||
auto buffer = ByteBuffer::create_uninitialized(stream.used_buffer_size()).release_value_but_fixme_should_propagate_errors();
|
||||
stream.read_until_filled(buffer).release_value_but_fixme_should_propagate_errors();
|
||||
argument_builder.append(StringView(buffer).trim_whitespace());
|
||||
++index;
|
||||
}
|
||||
dbgln("[wasm runtime] Stub function {} was called with the following arguments: {}", name, argument_builder.to_byte_string());
|
||||
Vector<Wasm::Value> result;
|
||||
result.ensure_capacity(type.results().size());
|
||||
for (auto& result_type : type.results())
|
||||
result.append(Wasm::Value { result_type, 0ull });
|
||||
for (size_t i = 0; i < type.results().size(); ++i)
|
||||
result.append(Wasm::Value());
|
||||
return Wasm::Result { move(result) };
|
||||
},
|
||||
type,
|
||||
|
@ -783,11 +818,11 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
|||
|
||||
for (auto& param : instance->get<Wasm::WasmFunction>().type().parameters()) {
|
||||
if (values_to_push.is_empty()) {
|
||||
values.append(Wasm::Value { param, 0ull });
|
||||
} else if (param == values_to_push.last().type()) {
|
||||
values.append(values_to_push.take_last());
|
||||
values.append(Wasm::Value());
|
||||
} else if (param == values_to_push.last().type) {
|
||||
values.append(values_to_push.take_last().value);
|
||||
} else {
|
||||
warnln("Type mismatch in argument: expected {}, but got {}", Wasm::ValueType::kind_name(param.kind()), Wasm::ValueType::kind_name(values_to_push.last().type().kind()));
|
||||
warnln("Type mismatch in argument: expected {}, but got {}", Wasm::ValueType::kind_name(param.kind()), Wasm::ValueType::kind_name(values_to_push.last().type.kind()));
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
@ -810,9 +845,12 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
|||
} else {
|
||||
if (!result.values().is_empty())
|
||||
warnln("Returned:");
|
||||
auto result_type = instance->get<Wasm::WasmFunction>().type().results();
|
||||
size_t index = 0;
|
||||
for (auto& value : result.values()) {
|
||||
g_stdout->write_until_depleted(" -> "sv.bytes()).release_value_but_fixme_should_propagate_errors();
|
||||
g_printer->print(value);
|
||||
g_printer->print(value, result_type[index]);
|
||||
++index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue