| // 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 |