// Copyright 2024 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. #ifndef V8_CCTEST_COMPILER_TURBOSHAFT_CODEGEN_TESTER_H_ #define V8_CCTEST_COMPILER_TURBOSHAFT_CODEGEN_TESTER_H_ #include "src/codegen/assembler.h" #include "src/codegen/optimized-compilation-info.h" #include "src/common/globals.h" #include "src/compiler/backend/instruction-selector.h" #include "src/compiler/compilation-dependencies.h" #include "src/compiler/linkage.h" #include "src/compiler/pipeline-data-inl.h" #include "src/compiler/pipeline.h" #include "src/compiler/turboshaft/assembler.h" #include "src/compiler/turboshaft/instruction-selection-phase.h" #include "src/compiler/turboshaft/load-store-simplification-reducer.h" #include "src/compiler/turboshaft/phase.h" #include "src/compiler/turboshaft/representations.h" #include "src/compiler/zone-stats.h" #include "src/objects/code-inl.h" #include "test/cctest/cctest.h" #include "test/common/call-tester.h" namespace v8::internal::compiler::turboshaft { using BaseAssembler = TSAssembler; class DataHolder { public: template DataHolder(Isolate* isolate, Zone* zone, MachineType return_type, ParamMachTypes... p) : isolate_(isolate), graph_zone_(zone), info_(zone->New(base::ArrayVector("testing"), zone, CodeKind::FOR_TESTING)), zone_stats_(isolate->allocator()), ts_pipeline_data_(&zone_stats_, turboshaft::TurboshaftPipelineKind::kJS, isolate, info_, AssemblerOptions::Default(isolate)), descriptor_(Linkage::GetSimplifiedCDescriptor( zone, CSignature::New(zone, return_type, p...), CallDescriptor::kInitializeRootRegister)) { ts_pipeline_data_.InitializeGraphComponent(nullptr); } PipelineData& ts_pipeline_data() { return ts_pipeline_data_; } Isolate* isolate() { return isolate_; } Zone* zone() { return graph_zone_; } Graph& graph() { return ts_pipeline_data_.graph(); } CallDescriptor* call_descriptor() { return descriptor_; } OptimizedCompilationInfo* info() { return info_; } private: Isolate* isolate_; Zone* graph_zone_; OptimizedCompilationInfo* info_; // zone_stats_ must be destroyed after pipeline_data_, so it's declared // before. ZoneStats zone_stats_; turboshaft::PipelineData ts_pipeline_data_; CallDescriptor* descriptor_; }; template class RawMachineAssemblerTester : public HandleAndZoneScope, public CallHelper, public DataHolder, public BaseAssembler { public: template explicit RawMachineAssemblerTester(ParamMachTypes... p) : HandleAndZoneScope(kCompressGraphZone), CallHelper( main_isolate(), CSignature::New(main_zone(), MachineTypeForC(), p...)), DataHolder(main_isolate(), main_zone(), MachineTypeForC(), p...), BaseAssembler(&DataHolder::ts_pipeline_data(), graph(), graph(), zone()) { Init(); } template RawMachineAssemblerTester(CodeKind kind, ParamMachTypes... p) : HandleAndZoneScope(kCompressGraphZone), CallHelper( main_isolate(), CSignature::New(main_zone(), MachineTypeForC(), p...)), DataHolder(main_isolate(), main_zone(), MachineTypeForC(), p...), BaseAssembler(&DataHolder::ts_pipeline_data(), graph(), graph(), zone()), kind_(kind) { Init(); } ~RawMachineAssemblerTester() override = default; void CheckNumber(double expected, Tagged number) { CHECK(Object::SameValue(*this->isolate()->factory()->NewNumber(expected), number)); } void CheckString(const char* expected, Tagged string) { CHECK(Object::SameValue( *this->isolate()->factory()->InternalizeUtf8String(expected), string)); } void GenerateCode() { Generate(); } DirectHandle GetCode() { Generate(); return code_.ToHandleChecked(); } using CallHelper::Call; using Assembler::Call; // A few Assembler helpers. using Assembler::Parameter; OpIndex Parameter(int i) { return Parameter(i, RegisterRepresentation::FromMachineType( call_descriptor()->GetParameterType(i))); } OpIndex PointerConstant(void* value) { return IntPtrConstant(reinterpret_cast(value)); } using Assembler::Load; OpIndex LoadFromPointer(void* address, MachineType type, int32_t offset = 0) { return Load(PointerConstant(address), LoadOp::Kind::RawAligned(), MemoryRepresentation::FromMachineType(type), offset); } OpIndex Load(MachineType type, OpIndex base) { MemoryRepresentation mem_rep = MemoryRepresentation::FromMachineType(type); return Load(base, LoadOp::Kind::RawAligned(), mem_rep); } using Assembler::Store; void StoreToPointer(void* address, MachineRepresentation rep, OpIndex value) { // Otherwise, we can use an offset instead of an Index. return Store(PointerConstant(address), value, StoreOp::Kind::RawAligned(), MemoryRepresentation::FromMachineRepresentation(rep), WriteBarrierKind::kNoWriteBarrier); } void Store(MachineRepresentation rep, OpIndex base, OpIndex value, WriteBarrierKind write_barrier) { MemoryRepresentation mem_rep = MemoryRepresentation::FromMachineRepresentation(rep); Store(base, value, StoreOp::Kind::RawAligned(), mem_rep, write_barrier); } V Int32GreaterThan(V a, V b) { return Int32LessThan(b, a); } V Int32GreaterThanOrEqual(V a, V b) { return Int32LessThanOrEqual(b, a); } V Uint32GreaterThan(V a, V b) { return Uint32LessThan(b, a); } V Uint32GreaterThanOrEqual(V a, V b) { return Uint32LessThanOrEqual(b, a); } protected: Address Generate() override { if (code_.is_null()) { code_ = Pipeline::GenerateTurboshaftCodeForTesting(call_descriptor(), &ts_pipeline_data()); } return code_.ToHandleChecked()->instruction_start(); } private: void Init() { // We bind a block right at the start so that the test can start emitting // operations without always needing to bind a block first. Block* start_block = NewBlock(); Bind(start_block); // We emit the parameters now so that they appear at the beginning of the // graph (because the register allocator doesn't like it when Parameters are // not in the 1st block). Subsequent calls to `m.Parameter()` will reuse the // Parameters created here, thanks to Turboshaft's parameter cache (see // TurboshaftAssemblerOpInterface::Parameter). for (size_t i = 0; i < call_descriptor()->ParameterCount(); i++) { Parameter(static_cast(i)); } } CodeKind kind_ = CodeKind::FOR_TESTING; MaybeHandle code_; }; template class BufferedRawMachineAssemblerTester : public RawMachineAssemblerTester { public: template explicit BufferedRawMachineAssemblerTester(ParamMachTypes... p) : RawMachineAssemblerTester( MachineType::Pointer(), ((void)p, MachineType::Pointer())...), test_graph_signature_( CSignature::New(this->main_zone(), MachineType::Int32(), p...)) { static_assert(sizeof...(p) <= arraysize(parameter_nodes_), "increase parameter_nodes_ array"); std::array p_arr{{p...}}; for (size_t i = 0; i < p_arr.size(); ++i) { parameter_nodes_[i] = Load( p_arr[i], RawMachineAssemblerTester::Parameter(static_cast(i))); } return_param_ = RawMachineAssemblerTester::Parameter(sizeof...(p)); } Address Generate() override { return RawMachineAssemblerTester::Generate(); } // The BufferedRawMachineAssemblerTester does not pass parameters directly // to the constructed IR graph. Instead it passes a pointer to the parameter // to the IR graph, and adds Load nodes to the IR graph to load the // parameters from memory. Thereby it is possible to pass 64 bit parameters // to the IR graph. OpIndex Parameter(size_t index) { CHECK_GT(arraysize(parameter_nodes_), index); return parameter_nodes_[index]; } // The BufferedRawMachineAssemblerTester adds a Store node to the IR graph // to store the graph's return value in memory. The memory address for the // Store node is provided as a parameter. By storing the return value in // memory it is possible to return 64 bit values. void Return(OpIndex input) { if (COMPRESS_POINTERS_BOOL && MachineTypeForC().IsTagged()) { // Since we are returning values via storing to off-heap location // generate full-word store here. Store(MachineType::PointerRepresentation(), return_param_, BitcastTaggedToWordPtr(input), kNoWriteBarrier); } else { Store(MachineTypeForC().representation(), return_param_, input, kNoWriteBarrier); } BaseAssembler::Return(Word32Constant(1234)); } template ReturnType Call(Params... p) { uintptr_t zap_data[] = {kZapValue, kZapValue}; ReturnType return_value; static_assert(sizeof(return_value) <= sizeof(zap_data)); MemCopy(&return_value, &zap_data, sizeof(return_value)); CSignature::VerifyParams(test_graph_signature_); CallHelper::Call(reinterpret_cast(&p)..., reinterpret_cast(&return_value)); return return_value; } private: CSignature* test_graph_signature_; OpIndex parameter_nodes_[4]; OpIndex return_param_; }; template <> class BufferedRawMachineAssemblerTester : public RawMachineAssemblerTester { public: template explicit BufferedRawMachineAssemblerTester(ParamMachTypes... p) : RawMachineAssemblerTester(((void)p, MachineType::Pointer())...), test_graph_signature_( CSignature::New(RawMachineAssemblerTester::main_zone(), MachineType::None(), p...)) { static_assert(sizeof...(p) <= arraysize(parameter_nodes_), "increase parameter_nodes_ array"); std::array p_arr{{p...}}; for (size_t i = 0; i < p_arr.size(); ++i) { parameter_nodes_[i] = Load(p_arr[i], Parameter(i)); } } Address Generate() override { return RawMachineAssemblerTester::Generate(); } // The BufferedRawMachineAssemblerTester does not pass parameters directly // to the constructed IR graph. Instead it passes a pointer to the parameter // to the IR graph, and adds Load nodes to the IR graph to load the // parameters from memory. Thereby it is possible to pass 64 bit parameters // to the IR graph. OpIndex Parameter(size_t index) { CHECK_GT(arraysize(parameter_nodes_), index); return parameter_nodes_[index]; } template void Call(Params... p) { CSignature::VerifyParams(test_graph_signature_); CallHelper::Call(reinterpret_cast(&p)...); } private: CSignature* test_graph_signature_; OpIndex parameter_nodes_[4]; }; static const bool USE_RESULT_BUFFER = true; static const bool USE_RETURN_REGISTER = false; static const int32_t CHECK_VALUE = 0x99BEEDCE; // TODO(titzer): use the C-style calling convention, or any register-based // calling convention for binop tests. template class BinopTester { public: explicit BinopTester(RawMachineAssemblerTester* tester, MachineType type) : T(tester), param0(T->LoadFromPointer(&p0, type)), param1(T->LoadFromPointer(&p1, type)), type(type), p0(static_cast(0)), p1(static_cast(0)), result(static_cast(0)) {} RawMachineAssemblerTester* T; OpIndex param0; OpIndex param1; CType call(CType a0, CType a1) { p0 = a0; p1 = a1; if (use_result_buffer) { CHECK_EQ(CHECK_VALUE, T->Call()); return result; } else { return static_cast(T->Call()); } } void AddReturn(OpIndex val) { if (use_result_buffer) { T->Store(type.representation(), T->PointerConstant(&result), T->Word32Constant(0), val, kNoWriteBarrier); T->Return(T->Word32Constant(CHECK_VALUE)); } else { T->Return(val); } } template void Run(const Ci& ci, const Cj& cj, const Fn& fn) { typename Ci::const_iterator i; typename Cj::const_iterator j; for (i = ci.begin(); i != ci.end(); ++i) { for (j = cj.begin(); j != cj.end(); ++j) { CHECK_EQ(fn(*i, *j), this->call(*i, *j)); } } } protected: MachineType type; CType p0; CType p1; CType result; }; // A helper class for testing code sequences that take two int parameters and // return an int value. class Int32BinopTester : public BinopTester { public: explicit Int32BinopTester(RawMachineAssemblerTester* tester) : BinopTester(tester, MachineType::Int32()) {} }; // A helper class for testing code sequences that take two int parameters and // return an int value. class Int64BinopTester : public BinopTester { public: explicit Int64BinopTester(RawMachineAssemblerTester* tester) : BinopTester(tester, MachineType::Int64()) {} }; // A helper class for testing code sequences that take two uint parameters and // return an uint value. class Uint32BinopTester : public BinopTester { public: explicit Uint32BinopTester(RawMachineAssemblerTester* tester) : BinopTester(tester, MachineType::Uint32()) {} uint32_t call(uint32_t a0, uint32_t a1) { p0 = a0; p1 = a1; return static_cast(T->Call()); } }; // A helper class for testing code sequences that take two float parameters and // return a float value. class Float32BinopTester : public BinopTester { public: explicit Float32BinopTester(RawMachineAssemblerTester* tester) : BinopTester(tester, MachineType::Float32()) {} }; // A helper class for testing code sequences that take two double parameters and // return a double value. class Float64BinopTester : public BinopTester { public: explicit Float64BinopTester(RawMachineAssemblerTester* tester) : BinopTester(tester, MachineType::Float64()) { } }; // A helper class for testing code sequences that take two pointer parameters // and return a pointer value. // TODO(titzer): pick word size of pointers based on V8_TARGET. template class PointerBinopTester : public BinopTester { public: explicit PointerBinopTester(RawMachineAssemblerTester* tester) : BinopTester(tester, MachineType::Pointer()) { } }; // A helper class for testing code sequences that take two tagged parameters and // return a tagged value. template class TaggedBinopTester : public BinopTester { public: explicit TaggedBinopTester(RawMachineAssemblerTester* tester) : BinopTester(tester, MachineType::AnyTagged()) {} }; #define BINOP_LIST(V) \ V(Word32Add) \ V(Word32Sub) \ V(Word32Mul) \ V(Word32BitwiseAnd) \ V(Word32BitwiseOr) \ V(Word32BitwiseXor) \ V(Word64Add) \ V(Word64Sub) \ V(Word64Mul) \ V(Word64BitwiseAnd) \ V(Word64BitwiseOr) \ V(Word64BitwiseXor) enum class TurboshaftBinop { #define DEF(kind) k##kind, BINOP_LIST(DEF) #undef DEF }; // A helper class for integer binary operations. Wraps a machine opcode and // provides evaluation routines and the operators. template class IntBinopWrapper { public: explicit IntBinopWrapper(TurboshaftBinop op) : op(op) {} OpIndex MakeNode(BaseAssembler& m, OpIndex a, OpIndex b) { return MakeNode(&m, a, b); } OpIndex MakeNode(BaseAssembler* m, OpIndex a, OpIndex b) { switch (op) { #define CASE(kind) \ case TurboshaftBinop::k##kind: \ return m->kind(a, b); BINOP_LIST(CASE) #undef CASE } } T eval(T a, T b) const { switch (op) { case TurboshaftBinop::kWord32Add: case TurboshaftBinop::kWord64Add: return a + b; case TurboshaftBinop::kWord32Sub: case TurboshaftBinop::kWord64Sub: return a - b; case TurboshaftBinop::kWord32Mul: case TurboshaftBinop::kWord64Mul: return a * b; case TurboshaftBinop::kWord32BitwiseAnd: case TurboshaftBinop::kWord64BitwiseAnd: return a & b; case TurboshaftBinop::kWord32BitwiseOr: case TurboshaftBinop::kWord64BitwiseOr: return a | b; case TurboshaftBinop::kWord32BitwiseXor: case TurboshaftBinop::kWord64BitwiseXor: return a ^ b; } } TurboshaftBinop op; }; #define COMPARE_LIST(V) \ V(TaggedEqual) \ V(Word32Equal) \ V(Int32LessThan) \ V(Int32LessThanOrEqual) \ V(Uint32LessThan) \ V(Uint32LessThanOrEqual) \ V(Word64Equal) \ V(Int64LessThan) \ V(Int64LessThanOrEqual) \ V(Uint64LessThan) \ V(Uint64LessThanOrEqual) \ V(Float64Equal) \ V(Float64LessThan) \ V(Float64LessThanOrEqual) enum class TurboshaftComparison { #define DEF(kind) k##kind, COMPARE_LIST(DEF) #undef DEF }; // A helper class for testing compares. Wraps a machine opcode and provides // evaluation routines and the operators. class CompareWrapper { public: explicit CompareWrapper(TurboshaftComparison op) : op(op) {} V MakeNode(BaseAssembler& m, OpIndex a, OpIndex b) { return MakeNode(&m, a, b); } V MakeNode(BaseAssembler* m, OpIndex a, OpIndex b) { switch (op) { #define CASE(kind) \ case TurboshaftComparison::k##kind: \ return m->kind(a, b); COMPARE_LIST(CASE) #undef CASE } } bool Int32Compare(int32_t a, int32_t b) const { switch (op) { case TurboshaftComparison::kWord32Equal: case TurboshaftComparison::kTaggedEqual: return a == b; case TurboshaftComparison::kInt32LessThan: return a < b; case TurboshaftComparison::kInt32LessThanOrEqual: return a <= b; case TurboshaftComparison::kUint32LessThan: return static_cast(a) < static_cast(b); case TurboshaftComparison::kUint32LessThanOrEqual: return static_cast(a) <= static_cast(b); default: UNREACHABLE(); } } bool Int64Compare(int64_t a, int64_t b) const { switch (op) { case TurboshaftComparison::kWord64Equal: case TurboshaftComparison::kTaggedEqual: return a == b; case TurboshaftComparison::kInt64LessThan: return a < b; case TurboshaftComparison::kInt64LessThanOrEqual: return a <= b; case TurboshaftComparison::kUint64LessThan: return static_cast(a) < static_cast(b); case TurboshaftComparison::kUint64LessThanOrEqual: return static_cast(a) <= static_cast(b); default: UNREACHABLE(); } } bool Float64Compare(double a, double b) const { switch (op) { case TurboshaftComparison::kFloat64Equal: return a == b; case TurboshaftComparison::kFloat64LessThan: return a < b; case TurboshaftComparison::kFloat64LessThanOrEqual: return a <= b; default: UNREACHABLE(); } } TurboshaftComparison op; }; // A small closure class to generate code for a function of two inputs that // produces a single output so that it can be used in many different contexts. // The {expected()} method should compute the expected output for a given // pair of inputs. template class BinopGen { public: virtual void gen(RawMachineAssemblerTester* m, OpIndex a, OpIndex b) = 0; virtual T expected(T a, T b) = 0; virtual ~BinopGen() = default; }; // A helper class to generate various combination of input shape combinations // and run the generated code to ensure it produces the correct results. class Int32BinopInputShapeTester { public: explicit Int32BinopInputShapeTester(BinopGen* g) : gen(g), input_a(0), input_b(0) {} void TestAllInputShapes(); private: BinopGen* gen; int32_t input_a; int32_t input_b; void Run(RawMachineAssemblerTester* m); void RunLeft(RawMachineAssemblerTester* m); void RunRight(RawMachineAssemblerTester* m); }; } // namespace v8::internal::compiler::turboshaft #endif // V8_CCTEST_COMPILER_TURBOSHAFT_CODEGEN_TESTER_H_