blob: 73855e91fcf2faae4beb4fd813fb6f067b8ecd77 [file] [log] [blame]
// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/wasm/baseline/liftoff-compiler.h"
#include "src/wasm/wasm-debug.h"
#include "test/cctest/cctest.h"
#include "test/cctest/wasm/wasm-run-utils.h"
#include "test/common/wasm/wasm-macro-gen.h"
namespace v8 {
namespace internal {
namespace wasm {
namespace {
class LiftoffCompileEnvironment {
public:
LiftoffCompileEnvironment()
: isolate_(CcTest::InitIsolateOnce()),
handle_scope_(isolate_),
zone_(isolate_->allocator(), ZONE_NAME),
module_builder_(&zone_, nullptr, TestExecutionTier::kLiftoff,
kRuntimeExceptionSupport, kNoLowerSimd) {
// Add a table of length 1, for indirect calls.
module_builder_.AddIndirectFunctionTable(nullptr, 1);
}
struct TestFunction {
OwnedVector<uint8_t> body_bytes;
WasmFunction* function;
FunctionBody body;
};
void CheckDeterministicCompilation(
std::initializer_list<ValueType> return_types,
std::initializer_list<ValueType> param_types,
std::initializer_list<uint8_t> raw_function_bytes) {
auto test_func = AddFunction(return_types, param_types, raw_function_bytes);
// Now compile the function with Liftoff two times.
CompilationEnv env = module_builder_.CreateCompilationEnv();
WasmFeatures detected1;
WasmFeatures detected2;
WasmCompilationResult result1 =
ExecuteLiftoffCompilation(isolate_->allocator(), &env, test_func.body,
test_func.function->func_index, kNoDebugging,
isolate_->counters(), &detected1);
WasmCompilationResult result2 =
ExecuteLiftoffCompilation(isolate_->allocator(), &env, test_func.body,
test_func.function->func_index, kNoDebugging,
isolate_->counters(), &detected2);
CHECK(result1.succeeded());
CHECK(result2.succeeded());
// Check that the generated code matches.
auto code1 =
VectorOf(result1.code_desc.buffer, result1.code_desc.instr_size);
auto code2 =
VectorOf(result2.code_desc.buffer, result2.code_desc.instr_size);
CHECK_EQ(code1, code2);
CHECK_EQ(detected1, detected2);
}
std::unique_ptr<DebugSideTable> GenerateDebugSideTable(
std::initializer_list<ValueType> return_types,
std::initializer_list<ValueType> param_types,
std::initializer_list<uint8_t> raw_function_bytes,
std::vector<int> breakpoints = {}) {
auto test_func = AddFunction(return_types, param_types, raw_function_bytes);
CompilationEnv env = module_builder_.CreateCompilationEnv();
WasmFeatures detected;
std::unique_ptr<DebugSideTable> debug_side_table_via_compilation;
ExecuteLiftoffCompilation(CcTest::i_isolate()->allocator(), &env,
test_func.body, 0, kForDebugging, nullptr,
&detected, VectorOf(breakpoints),
&debug_side_table_via_compilation);
// If there are no breakpoint, then {ExecuteLiftoffCompilation} should
// provide the same debug side table.
if (breakpoints.empty()) {
std::unique_ptr<DebugSideTable> debug_side_table =
GenerateLiftoffDebugSideTable(CcTest::i_isolate()->allocator(), &env,
test_func.body, 0);
CheckTableEquals(*debug_side_table, *debug_side_table_via_compilation);
}
return debug_side_table_via_compilation;
}
private:
static void CheckTableEquals(const DebugSideTable& a,
const DebugSideTable& b) {
CHECK_EQ(a.num_locals(), b.num_locals());
CHECK(std::equal(a.entries().begin(), a.entries().end(),
b.entries().begin(), b.entries().end(),
&CheckEntryEquals));
}
static bool CheckEntryEquals(const DebugSideTable::Entry& a,
const DebugSideTable::Entry& b) {
CHECK_EQ(a.pc_offset(), b.pc_offset());
CHECK(std::equal(a.values().begin(), a.values().end(), b.values().begin(),
b.values().end(), &CheckValueEquals));
return true;
}
static bool CheckValueEquals(const DebugSideTable::Entry::Value& a,
const DebugSideTable::Entry::Value& b) {
CHECK_EQ(a.type, b.type);
CHECK_EQ(a.kind, b.kind);
switch (a.kind) {
case DebugSideTable::Entry::kConstant:
CHECK_EQ(a.i32_const, b.i32_const);
break;
case DebugSideTable::Entry::kRegister:
CHECK_EQ(a.reg_code, b.reg_code);
break;
case DebugSideTable::Entry::kStack:
CHECK_EQ(a.stack_offset, b.stack_offset);
break;
}
return true;
}
OwnedVector<uint8_t> GenerateFunctionBody(
std::initializer_list<uint8_t> raw_function_bytes) {
// Build the function bytes by prepending the locals decl and appending an
// "end" opcode.
OwnedVector<uint8_t> function_bytes =
OwnedVector<uint8_t>::New(raw_function_bytes.size() + 2);
function_bytes[0] = WASM_NO_LOCALS;
std::copy(raw_function_bytes.begin(), raw_function_bytes.end(),
&function_bytes[1]);
function_bytes[raw_function_bytes.size() + 1] = WASM_END;
return function_bytes;
}
FunctionSig* AddSig(std::initializer_list<ValueType> return_types,
std::initializer_list<ValueType> param_types) {
ValueType* storage =
zone_.NewArray<ValueType>(return_types.size() + param_types.size());
std::copy(return_types.begin(), return_types.end(), storage);
std::copy(param_types.begin(), param_types.end(),
storage + return_types.size());
FunctionSig* sig = zone_.New<FunctionSig>(return_types.size(),
param_types.size(), storage);
module_builder_.AddSignature(sig);
return sig;
}
TestFunction AddFunction(std::initializer_list<ValueType> return_types,
std::initializer_list<ValueType> param_types,
std::initializer_list<uint8_t> raw_function_bytes) {
OwnedVector<uint8_t> function_bytes =
GenerateFunctionBody(raw_function_bytes);
FunctionSig* sig = AddSig(return_types, param_types);
int func_index =
module_builder_.AddFunction(sig, "f", TestingModuleBuilder::kWasm);
WasmFunction* function = module_builder_.GetFunctionAt(func_index);
function->code = {module_builder_.AddBytes(function_bytes.as_vector()),
static_cast<uint32_t>(function_bytes.size())};
FunctionBody body{function->sig, 0, function_bytes.begin(),
function_bytes.end()};
return {std::move(function_bytes), function, body};
}
Isolate* isolate_;
HandleScope handle_scope_;
Zone zone_;
TestingModuleBuilder module_builder_;
};
struct DebugSideTableEntry {
std::vector<DebugSideTable::Entry::Value> values;
// Construct via vector or implicitly via initializer list.
explicit DebugSideTableEntry(std::vector<DebugSideTable::Entry::Value> values)
: values(std::move(values)) {}
DebugSideTableEntry(
std::initializer_list<DebugSideTable::Entry::Value> values)
: values(values) {}
bool operator==(const DebugSideTableEntry& other) const {
if (values.size() != other.values.size()) return false;
for (size_t i = 0; i < values.size(); ++i) {
if (values[i].type != other.values[i].type) return false;
if (values[i].kind != other.values[i].kind) return false;
// Stack offsets and register codes are platform dependent, so only check
// constants here.
if (values[i].kind == DebugSideTable::Entry::kConstant &&
values[i].i32_const != other.values[i].i32_const) {
return false;
}
}
return true;
}
};
// Debug builds will print the vector of DebugSideTableEntry.
#ifdef DEBUG
std::ostream& operator<<(std::ostream& out, const DebugSideTableEntry& entry) {
out << "{";
const char* comma = "";
for (auto& v : entry.values) {
out << comma << v.type.name() << " ";
switch (v.kind) {
case DebugSideTable::Entry::kConstant:
out << "const:" << v.i32_const;
break;
case DebugSideTable::Entry::kRegister:
out << "reg";
break;
case DebugSideTable::Entry::kStack:
out << "stack";
break;
}
comma = ", ";
}
return out << "}";
}
std::ostream& operator<<(std::ostream& out,
const std::vector<DebugSideTableEntry>& entries) {
return out << PrintCollection(entries);
}
#endif // DEBUG
// Named constructors to make the tests more readable.
DebugSideTable::Entry::Value Constant(ValueType type, int32_t constant) {
DebugSideTable::Entry::Value value;
value.type = type;
value.kind = DebugSideTable::Entry::kConstant;
value.i32_const = constant;
return value;
}
DebugSideTable::Entry::Value Register(ValueType type) {
DebugSideTable::Entry::Value value;
value.type = type;
value.kind = DebugSideTable::Entry::kRegister;
return value;
}
DebugSideTable::Entry::Value Stack(ValueType type) {
DebugSideTable::Entry::Value value;
value.type = type;
value.kind = DebugSideTable::Entry::kStack;
return value;
}
void CheckDebugSideTable(std::vector<DebugSideTableEntry> expected_entries,
const wasm::DebugSideTable* debug_side_table) {
std::vector<DebugSideTableEntry> entries;
for (auto& entry : debug_side_table->entries()) {
auto values = entry.values();
entries.push_back(
DebugSideTableEntry{std::vector<DebugSideTable::Entry::Value>{
values.begin(), values.end()}});
}
CHECK_EQ(expected_entries, entries);
}
} // namespace
TEST(Liftoff_deterministic_simple) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_ADD(WASM_GET_LOCAL(0), WASM_GET_LOCAL(1))});
}
TEST(Liftoff_deterministic_call) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32},
{WASM_I32_ADD(WASM_CALL_FUNCTION(0, WASM_GET_LOCAL(0)),
WASM_GET_LOCAL(0))});
}
TEST(Liftoff_deterministic_indirect_call) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32},
{WASM_I32_ADD(WASM_CALL_INDIRECT(0, WASM_GET_LOCAL(0), WASM_I32V_1(47)),
WASM_GET_LOCAL(0))});
}
TEST(Liftoff_deterministic_loop) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32},
{WASM_LOOP(WASM_BR_IF(0, WASM_GET_LOCAL(0))), WASM_GET_LOCAL(0)});
}
TEST(Liftoff_deterministic_trap) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_DIVS(WASM_GET_LOCAL(0), WASM_GET_LOCAL(1))});
}
TEST(Liftoff_debug_side_table_simple) {
LiftoffCompileEnvironment env;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_ADD(WASM_GET_LOCAL(0), WASM_GET_LOCAL(1))});
CheckDebugSideTable(
{
// function entry, locals in registers.
{Register(kWasmI32), Register(kWasmI32)},
// OOL stack check, locals spilled, stack empty.
{Stack(kWasmI32), Stack(kWasmI32)},
},
debug_side_table.get());
}
TEST(Liftoff_debug_side_table_call) {
LiftoffCompileEnvironment env;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32},
{WASM_I32_ADD(WASM_CALL_FUNCTION(0, WASM_GET_LOCAL(0)),
WASM_GET_LOCAL(0))});
CheckDebugSideTable(
{
// function entry, local in register.
{Register(kWasmI32)},
// call, local spilled, stack empty.
{Stack(kWasmI32)},
// OOL stack check, local spilled, stack empty.
{Stack(kWasmI32)},
},
debug_side_table.get());
}
TEST(Liftoff_debug_side_table_call_const) {
LiftoffCompileEnvironment env;
constexpr int kConst = 13;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32},
{WASM_SET_LOCAL(0, WASM_I32V_1(kConst)),
WASM_I32_ADD(WASM_CALL_FUNCTION(0, WASM_GET_LOCAL(0)),
WASM_GET_LOCAL(0))});
CheckDebugSideTable(
{
// function entry, local in register.
{Register(kWasmI32)},
// call, local is kConst.
{Constant(kWasmI32, kConst)},
// OOL stack check, local spilled.
{Stack(kWasmI32)},
},
debug_side_table.get());
}
TEST(Liftoff_debug_side_table_indirect_call) {
LiftoffCompileEnvironment env;
constexpr int kConst = 47;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32},
{WASM_I32_ADD(WASM_CALL_INDIRECT(0, WASM_I32V_1(47), WASM_GET_LOCAL(0)),
WASM_GET_LOCAL(0))});
CheckDebugSideTable(
{
// function entry, local in register.
{Register(kWasmI32)},
// indirect call, local spilled, stack empty.
{Stack(kWasmI32)},
// OOL stack check, local spilled, stack empty.
{Stack(kWasmI32)},
// OOL trap (invalid index), local spilled, stack has {kConst}.
{Stack(kWasmI32), Constant(kWasmI32, kConst)},
// OOL trap (sig mismatch), local spilled, stack has {kConst}.
{Stack(kWasmI32), Constant(kWasmI32, kConst)},
},
debug_side_table.get());
}
TEST(Liftoff_debug_side_table_loop) {
LiftoffCompileEnvironment env;
constexpr int kConst = 42;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32},
{WASM_I32V_1(kConst), WASM_LOOP(WASM_BR_IF(0, WASM_GET_LOCAL(0)))});
CheckDebugSideTable(
{
// function entry, local in register.
{Register(kWasmI32)},
// OOL stack check, local spilled, stack empty.
{Stack(kWasmI32)},
// OOL loop stack check, local spilled, stack has {kConst}.
{Stack(kWasmI32), Constant(kWasmI32, kConst)},
},
debug_side_table.get());
}
TEST(Liftoff_debug_side_table_trap) {
LiftoffCompileEnvironment env;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_DIVS(WASM_GET_LOCAL(0), WASM_GET_LOCAL(1))});
CheckDebugSideTable(
{
// function entry, locals in registers.
{Register(kWasmI32), Register(kWasmI32)},
// OOL stack check, local spilled, stack empty.
{Stack(kWasmI32), Stack(kWasmI32)},
// OOL trap (div by zero), locals spilled, stack empty.
{Stack(kWasmI32), Stack(kWasmI32)},
// OOL trap (result unrepresentable), locals spilled, stack empty.
{Stack(kWasmI32), Stack(kWasmI32)},
},
debug_side_table.get());
}
TEST(Liftoff_breakpoint_simple) {
LiftoffCompileEnvironment env;
// Set two breakpoints. At both locations, values are live in registers.
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_ADD(WASM_GET_LOCAL(0), WASM_GET_LOCAL(1))},
{
1, // break at beginning of function (first local.get)
5 // break at i32.add
});
CheckDebugSideTable(
{
// First break point, locals in registers.
{Register(kWasmI32), Register(kWasmI32)},
// Second break point, locals and two stack values in registers.
{Register(kWasmI32), Register(kWasmI32), Register(kWasmI32),
Register(kWasmI32)},
// OOL stack check, locals spilled, stack empty.
{Stack(kWasmI32), Stack(kWasmI32)},
},
debug_side_table.get());
}
} // namespace wasm
} // namespace internal
} // namespace v8