v8/test/cctest/wasm/test-liftoff-inspection.cc
Clemens Backes 61150c17be [liftoff] Refactor options for Liftoff compilation
The number of arguments for the LiftoffCompiler has grown significantly
since its initial implementation, and it becomes hard to keep track of
all options at the call sites.

This CL refactors all optional parameters into a {LiftoffOptions} struct
which has a factory-like interface.
This will allow us to add more options in the future, e.g. for dynamic
tiering.

R=thibaudm@chromium.org

Change-Id: I66697bb2f99b676a84c158304cc3a285e1b077d0
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3069148
Commit-Queue: Clemens Backes <clemensb@chromium.org>
Reviewed-by: Thibaud Michaud <thibaudm@chromium.org>
Cr-Commit-Position: refs/heads/master@{#76098}
2021-08-04 16:48:48 +00:00

482 lines
17 KiB
C++

// 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/test-signatures.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),
wasm_runner_(nullptr, TestExecutionTier::kLiftoff, 0,
kRuntimeExceptionSupport) {
// Add a table of length 1, for indirect calls.
wasm_runner_.builder().AddIndirectFunctionTable(nullptr, 1);
// Set tiered down such that we generate debugging code.
wasm_runner_.builder().SetTieredDown();
}
struct TestFunction {
WasmCode* code;
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 = wasm_runner_.builder().CreateCompilationEnv();
WasmFeatures detected1;
WasmFeatures detected2;
WasmCompilationResult result1 = ExecuteLiftoffCompilation(
&env, test_func.body, test_func.code->index(), kNoDebugging,
LiftoffOptions{}.set_detected_features(&detected1));
WasmCompilationResult result2 = ExecuteLiftoffCompilation(
&env, test_func.body, test_func.code->index(), kNoDebugging,
LiftoffOptions{}.set_detected_features(&detected2));
CHECK(result1.succeeded());
CHECK(result2.succeeded());
// Check that the generated code matches.
auto code1 =
base::VectorOf(result1.code_desc.buffer, result1.code_desc.instr_size);
auto code2 =
base::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 = wasm_runner_.builder().CreateCompilationEnv();
std::unique_ptr<DebugSideTable> debug_side_table_via_compilation;
auto result = ExecuteLiftoffCompilation(
&env, test_func.body, 0, kForDebugging,
LiftoffOptions{}
.set_breakpoints(base::VectorOf(breakpoints))
.set_debug_sidetable(&debug_side_table_via_compilation));
CHECK(result.succeeded());
// 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(test_func.code);
CheckTableEquals(*debug_side_table, *debug_side_table_via_compilation);
}
return debug_side_table_via_compilation;
}
TestingModuleBuilder* builder() { return &wasm_runner_.builder(); }
private:
static void CheckTableEquals(const DebugSideTable& a,
const DebugSideTable& b) {
CHECK_EQ(a.num_locals(), b.num_locals());
CHECK_EQ(a.entries().size(), b.entries().size());
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_EQ(a.stack_height(), b.stack_height());
CHECK_EQ(a.changed_values(), b.changed_values());
return true;
}
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);
return sig;
}
TestFunction AddFunction(std::initializer_list<ValueType> return_types,
std::initializer_list<ValueType> param_types,
std::initializer_list<uint8_t> function_bytes) {
FunctionSig* sig = AddSig(return_types, param_types);
// Compile the function so we can get the WasmCode* which is later used to
// generate the debug side table lazily.
auto& func_compiler = wasm_runner_.NewFunction(sig, "f");
func_compiler.Build(function_bytes.begin(), function_bytes.end());
WasmCode* code =
wasm_runner_.builder().GetFunctionCode(func_compiler.function_index());
// Get the wire bytes created by the function compiler (including locals
// declaration and the trailing "end" opcode).
NativeModule* native_module = code->native_module();
auto* function = &native_module->module()->functions[code->index()];
base::Vector<const uint8_t> function_wire_bytes =
native_module->wire_bytes().SubVector(function->code.offset(),
function->code.end_offset());
FunctionBody body{sig, 0, function_wire_bytes.begin(),
function_wire_bytes.end()};
return {code, body};
}
Isolate* isolate_;
HandleScope handle_scope_;
Zone zone_;
// wasm_runner_ is used to build actual code objects needed to request lazy
// generation of debug side tables.
WasmRunnerBase wasm_runner_;
WasmCodeRefScope code_ref_scope_;
};
struct DebugSideTableEntry {
int stack_height;
std::vector<DebugSideTable::Entry::Value> changed_values;
// Construct via vector or implicitly via initializer list.
DebugSideTableEntry(int stack_height,
std::vector<DebugSideTable::Entry::Value> changed_values)
: stack_height(stack_height), changed_values(std::move(changed_values)) {}
DebugSideTableEntry(
int stack_height,
std::initializer_list<DebugSideTable::Entry::Value> changed_values)
: stack_height(stack_height), changed_values(changed_values) {}
bool operator==(const DebugSideTableEntry& other) const {
return stack_height == other.stack_height &&
std::equal(changed_values.begin(), changed_values.end(),
other.changed_values.begin(), other.changed_values.end(),
CheckValueEquals);
}
// Check for equality, but ignore exact register and stack offset.
static bool CheckValueEquals(const DebugSideTable::Entry::Value& a,
const DebugSideTable::Entry::Value& b) {
return a.index == b.index && a.type == b.type && a.storage == b.storage &&
(a.storage != DebugSideTable::Entry::kConstant ||
a.i32_const == b.i32_const);
}
};
// Debug builds will print the vector of DebugSideTableEntry.
#ifdef DEBUG
std::ostream& operator<<(std::ostream& out, const DebugSideTableEntry& entry) {
out << "stack height " << entry.stack_height << ", changed: {";
const char* comma = "";
for (auto& v : entry.changed_values) {
out << comma << v.index << ":" << v.type.name() << " ";
switch (v.storage) {
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(int index, ValueType type,
int32_t constant) {
DebugSideTable::Entry::Value value;
value.index = index;
value.type = type;
value.storage = DebugSideTable::Entry::kConstant;
value.i32_const = constant;
return value;
}
DebugSideTable::Entry::Value Register(int index, ValueType type) {
DebugSideTable::Entry::Value value;
value.index = index;
value.type = type;
value.storage = DebugSideTable::Entry::kRegister;
return value;
}
DebugSideTable::Entry::Value Stack(int index, ValueType type) {
DebugSideTable::Entry::Value value;
value.index = index;
value.type = type;
value.storage = 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()) {
entries.emplace_back(
entry.stack_height(),
std::vector<DebugSideTable::Entry::Value>{
entry.changed_values().begin(), entry.changed_values().end()});
}
CHECK_EQ(expected_entries, entries);
}
} // namespace
TEST(Liftoff_deterministic_simple) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_ADD(WASM_LOCAL_GET(0), WASM_LOCAL_GET(1))});
}
TEST(Liftoff_deterministic_call) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32},
{WASM_I32_ADD(WASM_CALL_FUNCTION(0, WASM_LOCAL_GET(0)),
WASM_LOCAL_GET(0))});
}
TEST(Liftoff_deterministic_indirect_call) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32},
{WASM_I32_ADD(WASM_CALL_INDIRECT(0, WASM_LOCAL_GET(0), WASM_I32V_1(47)),
WASM_LOCAL_GET(0))});
}
TEST(Liftoff_deterministic_loop) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32},
{WASM_LOOP(WASM_BR_IF(0, WASM_LOCAL_GET(0))), WASM_LOCAL_GET(0)});
}
TEST(Liftoff_deterministic_trap) {
LiftoffCompileEnvironment env;
env.CheckDeterministicCompilation(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_DIVS(WASM_LOCAL_GET(0), WASM_LOCAL_GET(1))});
}
TEST(Liftoff_debug_side_table_simple) {
LiftoffCompileEnvironment env;
auto debug_side_table = env.GenerateDebugSideTable(
{kWasmI32}, {kWasmI32, kWasmI32},
{WASM_I32_ADD(WASM_LOCAL_GET(0), WASM_LOCAL_GET(1))});
CheckDebugSideTable(
{
// function entry, locals in registers.
{2, {Register(0, kWasmI32), Register(1, kWasmI32)}},
// OOL stack check, locals spilled, stack still empty.
{2, {Stack(0, kWasmI32), Stack(1, 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_LOCAL_GET(0)),
WASM_LOCAL_GET(0))});
CheckDebugSideTable(
{
// function entry, local in register.
{1, {Register(0, kWasmI32)}},
// call, local spilled, stack empty.
{1, {Stack(0, kWasmI32)}},
// OOL stack check, local spilled as before, stack empty.
{1, {}},
},
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_LOCAL_SET(0, WASM_I32V_1(kConst)),
WASM_I32_ADD(WASM_CALL_FUNCTION(0, WASM_LOCAL_GET(0)),
WASM_LOCAL_GET(0))});
CheckDebugSideTable(
{
// function entry, local in register.
{1, {Register(0, kWasmI32)}},
// call, local is kConst.
{1, {Constant(0, kWasmI32, kConst)}},
// OOL stack check, local spilled.
{1, {Stack(0, 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_LOCAL_GET(0)),
WASM_LOCAL_GET(0))});
CheckDebugSideTable(
{
// function entry, local in register.
{1, {Register(0, kWasmI32)}},
// indirect call, local spilled, stack empty.
{1, {Stack(0, kWasmI32)}},
// OOL stack check, local still spilled.
{1, {}},
// OOL trap (invalid index), local still spilled, stack has {kConst}.
{2, {Constant(1, kWasmI32, kConst)}},
// OOL trap (sig mismatch), stack unmodified.
{2, {}},
},
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_LOCAL_GET(0)))});
CheckDebugSideTable(
{
// function entry, local in register.
{1, {Register(0, kWasmI32)}},
// OOL stack check, local spilled, stack empty.
{1, {Stack(0, kWasmI32)}},
// OOL loop stack check, local still spilled, stack has {kConst}.
{2, {Constant(1, 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_LOCAL_GET(0), WASM_LOCAL_GET(1))});
CheckDebugSideTable(
{
// function entry, locals in registers.
{2, {Register(0, kWasmI32), Register(1, kWasmI32)}},
// OOL stack check, local spilled, stack empty.
{2, {Stack(0, kWasmI32), Stack(1, kWasmI32)}},
// OOL trap (div by zero), stack as before.
{2, {}},
// OOL trap (unrepresentable), stack as before.
{2, {}},
},
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_LOCAL_GET(0), WASM_LOCAL_GET(1))},
{
1, // break at beginning of function (first local.get)
5 // break at i32.add
});
CheckDebugSideTable(
{
// First break point, locals in registers.
{2, {Register(0, kWasmI32), Register(1, kWasmI32)}},
// Second break point, locals unchanged, two register stack values.
{4, {Register(2, kWasmI32), Register(3, kWasmI32)}},
// OOL stack check, locals spilled, stack empty.
{2, {Stack(0, kWasmI32), Stack(1, kWasmI32)}},
},
debug_side_table.get());
}
TEST(Liftoff_debug_side_table_catch_all) {
EXPERIMENTAL_FLAG_SCOPE(eh);
LiftoffCompileEnvironment env;
TestSignatures sigs;
int ex = env.builder()->AddException(sigs.v_v());
ValueType exception_type = ValueType::Ref(HeapType::kExtern, kNonNullable);
auto debug_side_table = env.GenerateDebugSideTable(
{}, {kWasmI32},
{WASM_TRY_CATCH_ALL_T(kWasmI32, WASM_STMTS(WASM_I32V(0), WASM_THROW(ex)),
WASM_I32V(1)),
WASM_DROP},
{
18 // Break at the end of the try block.
});
CheckDebugSideTable(
{
// function entry.
{1, {Register(0, kWasmI32)}},
// breakpoint.
{3,
{Stack(0, kWasmI32), Register(1, exception_type),
Constant(2, kWasmI32, 1)}},
{1, {}},
},
debug_side_table.get());
}
TEST(Regress1199526) {
EXPERIMENTAL_FLAG_SCOPE(eh);
LiftoffCompileEnvironment env;
ValueType exception_type = ValueType::Ref(HeapType::kExtern, kNonNullable);
auto debug_side_table = env.GenerateDebugSideTable(
{}, {},
{kExprTry, kVoidCode, kExprCallFunction, 0, kExprCatchAll, kExprLoop,
kVoidCode, kExprEnd, kExprEnd},
{});
CheckDebugSideTable(
{
// function entry.
{0, {}},
// break on entry.
{0, {}},
// function call.
{0, {}},
// loop stack check.
{1, {Stack(0, exception_type)}},
},
debug_side_table.get());
}
} // namespace wasm
} // namespace internal
} // namespace v8