// 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 body_bytes; WasmFunction* function; FunctionBody body; }; void CheckDeterministicCompilation( std::initializer_list return_types, std::initializer_list param_types, std::initializer_list 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 GenerateDebugSideTable( std::initializer_list return_types, std::initializer_list param_types, std::initializer_list raw_function_bytes, std::vector breakpoints = {}) { auto test_func = AddFunction(return_types, param_types, raw_function_bytes); CompilationEnv env = module_builder_.CreateCompilationEnv(); WasmFeatures detected; std::unique_ptr 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 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 GenerateFunctionBody( std::initializer_list raw_function_bytes) { // Build the function bytes by prepending the locals decl and appending an // "end" opcode. OwnedVector function_bytes = OwnedVector::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 return_types, std::initializer_list param_types) { ValueType* storage = zone_.NewArray(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(return_types.size(), param_types.size(), storage); module_builder_.AddSignature(sig); return sig; } TestFunction AddFunction(std::initializer_list return_types, std::initializer_list param_types, std::initializer_list raw_function_bytes) { OwnedVector 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(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 values; // Construct via vector or implicitly via initializer list. explicit DebugSideTableEntry(std::vector values) : values(std::move(values)) {} DebugSideTableEntry( std::initializer_list 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& 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 expected_entries, const wasm::DebugSideTable* debug_side_table) { std::vector entries; for (auto& entry : debug_side_table->entries()) { auto values = entry.values(); entries.push_back( DebugSideTableEntry{std::vector{ 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