// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. #ifndef RUNTIME_VM_COMPILER_BACKEND_IL_TEST_HELPER_H_ #define RUNTIME_VM_COMPILER_BACKEND_IL_TEST_HELPER_H_ #include #include #include #include "include/dart_api.h" #include "platform/allocation.h" #include "vm/compiler/backend/flow_graph.h" #include "vm/compiler/backend/il.h" #include "vm/compiler/compiler_pass.h" #include "vm/compiler/compiler_state.h" #include "vm/compiler/jit/compiler.h" #include "vm/unit_test.h" // The helpers in this file make it easier to write C++ unit tests which assert // that Dart code gets turned into certain IR. // // Here is an example on how to use it: // // ISOLATE_UNIT_TEST_CASE(MyIRTest) { // const char* script = R"( // void foo() { ... } // void main() { foo(); } // )"; // // // Load the script and exercise the code once. // const auto& lib = Library::Handle(LoadTestScript(script); // // // Cause the code to be exercised once (to populate ICData). // Invoke(lib, "main"); // // // Look up the function. // const auto& function = Function::Handle(GetFunction(lib, "foo")); // // // Run the JIT compilation pipeline with two passes. // TestPipeline pipeline(function); // FlowGraph* graph = pipeline.RunJITPasses("ComputeSSA,TypePropagation"); // // ... // } // namespace dart { class FlowGraph; class Function; class Library; LibraryPtr LoadTestScript(const char* script, Dart_NativeEntryResolver resolver = nullptr, const char* lib_uri = RESOLVED_USER_TEST_URI); FunctionPtr GetFunction(const Library& lib, const char* name); ClassPtr GetClass(const Library& lib, const char* name); TypeParameterPtr GetClassTypeParameter(const Class& klass, const char* name); TypeParameterPtr GetFunctionTypeParameter(const Function& fun, const char* name); ObjectPtr Invoke(const Library& lib, const char* name); class TestPipeline : public ValueObject { public: explicit TestPipeline(const Function& function, CompilerPass::PipelineMode mode, bool is_optimizing = true) : function_(function), thread_(Thread::Current()), compiler_state_(thread_, mode == CompilerPass::PipelineMode::kAOT, is_optimizing, CompilerState::ShouldTrace(function)), mode_(mode) {} ~TestPipeline() { delete pass_state_; } // As a side-effect this will populate // - [ic_data_array_] // - [parsed_function_] // - [pass_state_] // - [flow_graph_] FlowGraph* RunPasses(std::initializer_list passes); void CompileGraphAndAttachFunction(); private: const Function& function_; Thread* thread_; CompilerState compiler_state_; CompilerPass::PipelineMode mode_; ZoneGrowableArray* ic_data_array_ = nullptr; ParsedFunction* parsed_function_ = nullptr; CompilerPassState* pass_state_ = nullptr; FlowGraph* flow_graph_ = nullptr; }; // Match opcodes used for [ILMatcher], see below. enum MatchOpCode { // Emit a match and match-and-move code for every instruction. #define DEFINE_MATCH_OPCODES(Instruction, _) \ kMatch##Instruction, kMatchAndMove##Instruction, \ kMatchAndMoveOptional##Instruction, FOR_EACH_INSTRUCTION(DEFINE_MATCH_OPCODES) #undef DEFINE_MATCH_OPCODES // Matches a branch and moves left. kMatchAndMoveBranchTrue, // Matches a branch and moves right. kMatchAndMoveBranchFalse, // Is ignored. kNop, // Moves forward across any instruction. kMoveAny, // Moves over all parallel moves. kMoveParallelMoves, // Moves forward until the next match code matches. kMoveGlob, // Moves over any DebugStepChecks. kMoveDebugStepChecks, // Invalid match opcode used as default [insert_before] argument to TryMatch // to signal that no insertions should occur. kInvalidMatchOpCode, }; // Match codes used for [ILMatcher], see below. class MatchCode { public: MatchCode(MatchOpCode opcode) // NOLINT : opcode_(opcode), capture_(nullptr) {} MatchCode(MatchOpCode opcode, Instruction** capture) : opcode_(opcode), capture_(capture) {} #define DEFINE_TYPED_CONSTRUCTOR(Type, ignored) \ MatchCode(MatchOpCode opcode, Type##Instr** capture) \ : opcode_(opcode), capture_(reinterpret_cast(capture)) { \ RELEASE_ASSERT(opcode == kMatch##Type || opcode == kMatchAndMove##Type); \ } FOR_EACH_INSTRUCTION(DEFINE_TYPED_CONSTRUCTOR) #undef DEFINE_TYPED_CONSTRUCTOR MatchOpCode opcode() { return opcode_; } private: friend class ILMatcher; MatchOpCode opcode_; Instruction** capture_; }; enum class ParallelMovesHandling { // Matcher doesn't do anything special with ParallelMove instructions. kDefault, // All ParallelMove instructions are skipped. // This mode is useful when matching a flow graph after the whole // compiler pipeline, as it may have ParallelMove instructions // at arbitrary architecture-dependent places. kSkip, }; // Used for matching a sequence of IL instructions including capturing support. // // Example: // // TargetEntryInstr* entry = ....; // BranchInstr* branch = nullptr; // // ILMatcher matcher(flow_graph, entry); // if (matcher.TryMatch({ kMoveGlob, {kMatchBranch, &branch}, })) { // EXPECT(branch->operation_cid() == kMintCid); // ... // } // // This match will start at [entry], follow any number instructions (including // [GotoInstr]s until a [BranchInstr] is found). // // If the match was successful, this returns `true` and updates the current // value for the cursor. class ILMatcher : public ValueObject { public: ILMatcher(FlowGraph* flow_graph, Instruction* cursor, bool trace = true, ParallelMovesHandling parallel_moves_handling = ParallelMovesHandling::kDefault) : flow_graph_(flow_graph), cursor_(cursor), parallel_moves_handling_(parallel_moves_handling), // clang-format off #if !defined(PRODUCT) trace_(trace) {} #else trace_(false) {} #endif // clang-format on Instruction* value() { return cursor_; } // From the current [value] according to match_codes. // // Returns `true` if the match was successful and cursor has been updated, // otherwise returns `false`. // // If [insert_before] is a valid match opcode, then it will be inserted // before each MatchCode in [match_codes] prior to matching. bool TryMatch(std::initializer_list match_codes, MatchOpCode insert_before = kInvalidMatchOpCode); private: Instruction* MatchInternal(std::vector match_codes, size_t i, Instruction* cursor); const char* MatchOpCodeToCString(MatchOpCode code); FlowGraph* flow_graph_; Instruction* cursor_; ParallelMovesHandling parallel_moves_handling_; bool trace_; }; #if !defined(PRODUCT) #define ENTITY_TOCSTRING(v) ((v)->ToCString()) #else #define ENTITY_TOCSTRING(v) "" #endif // Helper to check various IL and object properties and informative error // messages if check fails. [entity] should be a pointer to a value. // [property] should be an expression which can refer to [entity] using // variable named [it]. // [entity] is expected to have a ToCString() method in non-PRODUCT builds. #define EXPECT_PROPERTY(entity, property) \ do { \ auto& it = *entity; \ if (!(property)) { \ dart::Expect(__FILE__, __LINE__) \ .Fail("expected " #property " for " #entity " which is %s.\n", \ ENTITY_TOCSTRING(entity)); \ } \ } while (0) class FlowGraphBuilderHelper { public: FlowGraphBuilderHelper() : state_(CompilerState::Current()), flow_graph_(MakeDummyGraph(Thread::Current())) { flow_graph_.CreateCommonConstants(); } TargetEntryInstr* TargetEntry(intptr_t try_index = kInvalidTryIndex) const { return new TargetEntryInstr(flow_graph_.allocate_block_id(), try_index, state_.GetNextDeoptId()); } JoinEntryInstr* JoinEntry(intptr_t try_index = kInvalidTryIndex) const { return new JoinEntryInstr(flow_graph_.allocate_block_id(), try_index, state_.GetNextDeoptId()); } ConstantInstr* IntConstant(int64_t value) const { return flow_graph_.GetConstant( Integer::Handle(Integer::NewCanonical(value))); } ConstantInstr* DoubleConstant(double value) { return flow_graph_.GetConstant(Double::Handle(Double::NewCanonical(value))); } enum class IncomingDefKind { kImmediate, kDelayed, }; class IncomingDef { public: IncomingDef(BlockEntryInstr* from, Definition* defn) : kind_(IncomingDefKind::kImmediate), from_(from), defn_(defn) {} template ::value>::type> IncomingDef(BlockEntryInstr* from, T** defn_source) : kind_(IncomingDefKind::kDelayed), from_(from), defn_source_(reinterpret_cast(defn_source)) {} BlockEntryInstr* from() const { return from_; } Definition* defn() const { return kind_ == IncomingDefKind::kImmediate ? defn_ : *defn_source_; } private: IncomingDefKind kind_; BlockEntryInstr* from_; union { Definition* defn_; Definition** defn_source_; }; }; PhiInstr* Phi(JoinEntryInstr* join, std::initializer_list incoming) { auto phi = new PhiInstr(join, incoming.size()); for (size_t i = 0; i < incoming.size(); i++) { auto input = new Value(flow_graph_.constant_dead()); phi->SetInputAt(i, input); input->definition()->AddInputUse(input); } for (auto def : incoming) { pending_phis_.Add({phi, def}); } return phi; } void FinishGraph() { flow_graph_.DiscoverBlocks(); GrowableArray dominance_frontier; flow_graph_.ComputeDominators(&dominance_frontier); for (auto& pending : pending_phis_) { auto join = pending.phi->block(); EXPECT(pending.phi->InputCount() == join->PredecessorCount()); auto pred_index = join->IndexOfPredecessor(pending.incoming.from()); EXPECT(pred_index != -1); pending.phi->InputAt(pred_index)->BindTo(pending.incoming.defn()); } } FlowGraph* flow_graph() { return &flow_graph_; } private: static FlowGraph& MakeDummyGraph(Thread* thread) { const FunctionType& signature = FunctionType::ZoneHandle(FunctionType::New()); const Function& func = Function::ZoneHandle(Function::New( signature, String::Handle(Symbols::New(thread, "dummy")), UntaggedFunction::kRegularFunction, /*is_static=*/true, /*is_const=*/false, /*is_abstract=*/false, /*is_external=*/false, /*is_native=*/true, Class::Handle(thread->isolate_group()->object_store()->object_class()), TokenPosition::kNoSource)); Zone* zone = thread->zone(); ParsedFunction* parsed_function = new (zone) ParsedFunction(thread, func); parsed_function->set_scope(new LocalScope(nullptr, 0, 0)); auto graph_entry = new GraphEntryInstr(*parsed_function, Compiler::kNoOSRDeoptId); const intptr_t block_id = 1; // 0 is GraphEntry. graph_entry->set_normal_entry( new FunctionEntryInstr(graph_entry, block_id, kInvalidTryIndex, CompilerState::Current().GetNextDeoptId())); return *new FlowGraph(*parsed_function, graph_entry, block_id, PrologueInfo{-1, -1}); } CompilerState& state_; FlowGraph& flow_graph_; struct PendingPhiInput { PhiInstr* phi; IncomingDef incoming; }; GrowableArray pending_phis_; }; } // namespace dart #endif // RUNTIME_VM_COMPILER_BACKEND_IL_TEST_HELPER_H_