blob: 01da0c382ba2f3d925f9205721d6e918e68fa2ab [file] [log] [blame]
// 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 <utility>
#include <vector>
#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)
: function_(function),
thread_(Thread::Current()),
compiler_state_(thread_,
mode == CompilerPass::PipelineMode::kAOT,
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<CompilerPass::Id> passes);
void CompileGraphAndAttachFunction();
private:
const Function& function_;
Thread* thread_;
CompilerState compiler_state_;
CompilerPass::PipelineMode mode_;
ZoneGrowableArray<const ICData*>* 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<Instruction**>(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<MatchCode> match_codes,
MatchOpCode insert_before = kInvalidMatchOpCode);
private:
Instruction* MatchInternal(std::vector<MatchCode> 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)));
}
static Definition* const kPhiSelfReference;
PhiInstr* Phi(JoinEntryInstr* join,
std::initializer_list<std::pair<BlockEntryInstr*, Definition*>>
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 pair : incoming) {
pending_phis_.Add({phi, pair.first,
pair.second == kPhiSelfReference ? phi : pair.second});
}
return phi;
}
void FinishGraph() {
flow_graph_.DiscoverBlocks();
GrowableArray<BitVector*> 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.pred);
EXPECT(pred_index != -1);
pending.phi->InputAt(pred_index)->BindTo(pending.defn);
}
}
FlowGraph* flow_graph() { return &flow_graph_; }
private:
static FlowGraph& MakeDummyGraph(Thread* thread) {
const Function& func = Function::ZoneHandle(Function::New(
String::Handle(Symbols::New(thread, "dummy")),
FunctionLayout::kRegularFunction,
/*is_static=*/true,
/*is_const=*/false,
/*is_abstract=*/false,
/*is_external=*/false,
/*is_native=*/true,
Class::Handle(thread->isolate()->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;
BlockEntryInstr* pred;
Definition* defn;
};
GrowableArray<PendingPhiInput> pending_phis_;
};
} // namespace dart
#endif // RUNTIME_VM_COMPILER_BACKEND_IL_TEST_HELPER_H_