| // Copyright (c) 2018, 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_FRONTEND_BASE_FLOW_GRAPH_BUILDER_H_ | |
| #define RUNTIME_VM_COMPILER_FRONTEND_BASE_FLOW_GRAPH_BUILDER_H_ | |
| #if defined(DART_PRECOMPILED_RUNTIME) | |
| #error "AOT runtime should not use compiler sources (including header files)" | |
| #endif // defined(DART_PRECOMPILED_RUNTIME) | |
| #include <initializer_list> | |
| #include "vm/compiler/backend/flow_graph.h" | |
| #include "vm/compiler/backend/il.h" | |
| #include "vm/object.h" | |
| namespace dart { | |
| class InlineExitCollector; | |
| namespace kernel { | |
| class BaseFlowGraphBuilder; | |
| struct InferredTypeMetadata; | |
| class TryCatchBlock; | |
| class Fragment { | |
| public: | |
| Instruction* entry = nullptr; | |
| Instruction* current = nullptr; | |
| Fragment() {} | |
| explicit Fragment(Instruction* instruction) | |
| : entry(instruction), current(instruction) {} | |
| Fragment(Instruction* entry, Instruction* current) | |
| : entry(entry), current(current) { | |
| ASSERT(is_valid()); | |
| } | |
| bool is_open() const { return entry == nullptr || current != nullptr; } | |
| bool is_closed() const { return !is_open(); } | |
| bool is_empty() const { return entry == nullptr && current == nullptr; } | |
| // Non-empty fragment should have an entry. | |
| bool is_valid() const { return is_empty() || (entry != nullptr); } | |
| void Prepend(Instruction* start); | |
| Fragment& operator+=(const Fragment& other); | |
| Fragment& operator<<=(Instruction* next); | |
| Fragment closed(); | |
| private: | |
| DISALLOW_ALLOCATION(); | |
| }; | |
| Fragment operator+(const Fragment& first, const Fragment& second); | |
| Fragment operator<<(const Fragment& fragment, Instruction* next); | |
| // IL fragment that performs some sort of test (comparison) and | |
| // has a single entry and multiple true and false exits. | |
| class TestFragment { | |
| public: | |
| BlockEntryInstr* CreateTrueSuccessor(BaseFlowGraphBuilder* builder); | |
| BlockEntryInstr* CreateFalseSuccessor(BaseFlowGraphBuilder* builder); | |
| void IfTrueGoto(BaseFlowGraphBuilder* builder, JoinEntryInstr* join) { | |
| ConnectBranchesTo(builder, *true_successor_addresses, join); | |
| } | |
| // If negate is true then return negated fragment by flipping | |
| // true and false successors. Otherwise return this fragment | |
| // without change. | |
| TestFragment Negate(bool negate) { | |
| if (negate) { | |
| return TestFragment(entry, false_successor_addresses, | |
| true_successor_addresses); | |
| } else { | |
| return *this; | |
| } | |
| } | |
| typedef ZoneGrowableArray<TargetEntryInstr**> SuccessorAddressArray; | |
| // Create an empty fragment. | |
| TestFragment() {} | |
| // Create a fragment with the given entry and true/false exits. | |
| TestFragment(Instruction* entry, | |
| SuccessorAddressArray* true_successor_addresses, | |
| SuccessorAddressArray* false_successor_addresses) | |
| : entry(entry), | |
| true_successor_addresses(true_successor_addresses), | |
| false_successor_addresses(false_successor_addresses) {} | |
| // Create a fragment with the given entry and a single branch as an exit. | |
| TestFragment(Instruction* entry, BranchInstr* branch); | |
| void ConnectBranchesTo(BaseFlowGraphBuilder* builder, | |
| const TestFragment::SuccessorAddressArray& branches, | |
| JoinEntryInstr* join); | |
| BlockEntryInstr* CreateSuccessorFor( | |
| BaseFlowGraphBuilder* builder, | |
| const TestFragment::SuccessorAddressArray& branches); | |
| Instruction* entry = nullptr; | |
| SuccessorAddressArray* true_successor_addresses = nullptr; | |
| SuccessorAddressArray* false_successor_addresses = nullptr; | |
| }; | |
| // Indicates which form of the unchecked entrypoint we are compiling. | |
| // | |
| // kNone: | |
| // | |
| // There is no unchecked entrypoint: the unchecked entry is set to nullptr in | |
| // the 'GraphEntryInstr'. | |
| // | |
| // kSeparate: | |
| // | |
| // The normal and unchecked entrypoint each point to their own versions of | |
| // the prologue, containing exactly those checks which need to be performed | |
| // on either side. Both sides jump directly to the body after performing | |
| // their prologue. | |
| // | |
| // kSharedWithVariable: | |
| // | |
| // A temporary variable is allocated and initialized to 0 on normal entry | |
| // and 2 on unchecked entry. Code which should be omitted on the unchecked | |
| // entrypoint is made conditional on this variable being equal to 0. | |
| // | |
| enum class UncheckedEntryPointStyle { | |
| kNone = 0, | |
| kSeparate = 1, | |
| kSharedWithVariable = 2, | |
| }; | |
| class BaseFlowGraphBuilder { | |
| public: | |
| BaseFlowGraphBuilder( | |
| const ParsedFunction* parsed_function, | |
| bool optimizing, | |
| intptr_t last_used_block_id, | |
| intptr_t osr_id = DeoptId::kNone, | |
| ZoneGrowableArray<intptr_t>* context_level_array = nullptr, | |
| InlineExitCollector* exit_collector = nullptr, | |
| bool inlining_unchecked_entry = false, | |
| const Function* caller = nullptr) | |
| : parsed_function_(parsed_function), | |
| function_(parsed_function_->function()), | |
| optimizing_(optimizing), | |
| should_omit_stack_overflow_checks_( | |
| ShouldOmitStackOverflowChecks(optimizing, | |
| parsed_function->function())), | |
| should_omit_check_bounds_( | |
| ShouldOmitCheckBoundsIn(parsed_function->function(), caller)), | |
| thread_(Thread::Current()), | |
| zone_(thread_->zone()), | |
| osr_id_(osr_id), | |
| context_level_array_(context_level_array), | |
| context_depth_(0), | |
| last_used_block_id_(last_used_block_id), | |
| current_try_index_(kInvalidTryIndex), | |
| next_used_try_index_(0), | |
| stack_(nullptr), | |
| exit_collector_(exit_collector), | |
| inlining_unchecked_entry_(inlining_unchecked_entry), | |
| saved_args_desc_array_( | |
| has_saved_args_desc_array() | |
| ? Array::ZoneHandle(zone_, function_.saved_args_desc()) | |
| : Object::null_array()), | |
| coverage_array_(TypedData::ZoneHandle( | |
| zone_, | |
| parsed_function->function().GetCoverageArray())) {} | |
| const TypedData& coverage_array() const { return coverage_array_; } | |
| void FinalizeCoverageArray(); | |
| Fragment LoadField(const Field& field, bool calls_initializer); | |
| Fragment LoadNativeField(const Slot& native_field, | |
| InnerPointerAccess loads_inner_pointer, | |
| bool calls_initializer = false, | |
| compiler::Assembler::MemoryOrder memory_order = | |
| compiler::Assembler::kRelaxedNonAtomic); | |
| Fragment LoadNativeField(const Slot& native_field, | |
| bool calls_initializer = false, | |
| compiler::Assembler::MemoryOrder memory_order = | |
| compiler::Assembler::kRelaxedNonAtomic); | |
| // Pass true for index_unboxed if indexing into external typed data. | |
| Fragment LoadIndexed(classid_t class_id, | |
| intptr_t index_scale = compiler::target::kWordSize, | |
| bool index_unboxed = false, | |
| AlignmentType alignment = kAlignedAccess); | |
| Fragment GenericCheckBound(); | |
| Fragment LoadUntagged(intptr_t offset); | |
| Fragment CalculateElementAddress(intptr_t index_scale); | |
| Fragment ConvertUntaggedToUnboxed(); | |
| Fragment ConvertUnboxedToUntagged(); | |
| Fragment FloatToDouble(); | |
| Fragment DoubleToFloat(); | |
| void SetTempIndex(Definition* definition); | |
| Fragment LoadLocal(LocalVariable* variable); | |
| Fragment StoreLocal(LocalVariable* variable) { | |
| return StoreLocal(TokenPosition::kNoSource, variable); | |
| } | |
| Fragment StoreLocal(TokenPosition position, LocalVariable* variable); | |
| Fragment StoreLocalRaw(TokenPosition position, LocalVariable* variable); | |
| Fragment LoadContextAt(int depth); | |
| Fragment GuardFieldLength(const Field& field, intptr_t deopt_id); | |
| Fragment GuardFieldClass(const Field& field, intptr_t deopt_id); | |
| static const Field& MayCloneField(Zone* zone, const Field& field); | |
| Fragment StoreNativeField( | |
| TokenPosition position, | |
| const Slot& slot, | |
| InnerPointerAccess stores_inner_pointer, | |
| StoreFieldInstr::Kind kind = StoreFieldInstr::Kind::kOther, | |
| StoreBarrierType emit_store_barrier = kEmitStoreBarrier, | |
| compiler::Assembler::MemoryOrder memory_order = | |
| compiler::Assembler::kRelaxedNonAtomic); | |
| Fragment StoreNativeField( | |
| TokenPosition position, | |
| const Slot& slot, | |
| StoreFieldInstr::Kind kind = StoreFieldInstr::Kind::kOther, | |
| StoreBarrierType emit_store_barrier = kEmitStoreBarrier, | |
| compiler::Assembler::MemoryOrder memory_order = | |
| compiler::Assembler::kRelaxedNonAtomic) { | |
| return StoreNativeField(position, slot, InnerPointerAccess::kNotUntagged, | |
| kind, emit_store_barrier, memory_order); | |
| } | |
| Fragment StoreNativeField( | |
| const Slot& slot, | |
| InnerPointerAccess stores_inner_pointer, | |
| StoreFieldInstr::Kind kind = StoreFieldInstr::Kind::kOther, | |
| StoreBarrierType emit_store_barrier = kEmitStoreBarrier, | |
| compiler::Assembler::MemoryOrder memory_order = | |
| compiler::Assembler::kRelaxedNonAtomic) { | |
| return StoreNativeField(TokenPosition::kNoSource, slot, | |
| stores_inner_pointer, kind, emit_store_barrier, | |
| memory_order); | |
| } | |
| Fragment StoreNativeField( | |
| const Slot& slot, | |
| StoreFieldInstr::Kind kind = StoreFieldInstr::Kind::kOther, | |
| StoreBarrierType emit_store_barrier = kEmitStoreBarrier, | |
| compiler::Assembler::MemoryOrder memory_order = | |
| compiler::Assembler::kRelaxedNonAtomic) { | |
| return StoreNativeField(TokenPosition::kNoSource, slot, | |
| InnerPointerAccess::kNotUntagged, kind, | |
| emit_store_barrier, memory_order); | |
| } | |
| Fragment StoreField( | |
| const Field& field, | |
| StoreFieldInstr::Kind kind = StoreFieldInstr::Kind::kOther, | |
| StoreBarrierType emit_store_barrier = kEmitStoreBarrier); | |
| Fragment StoreFieldGuarded( | |
| const Field& field, | |
| StoreFieldInstr::Kind kind = StoreFieldInstr::Kind::kOther, | |
| bool requires_immutability_check = false); | |
| Fragment LoadStaticField(const Field& field, bool calls_initializer); | |
| Fragment RedefinitionWithType(const AbstractType& type); | |
| Fragment ReachabilityFence(); | |
| Fragment StoreStaticField(TokenPosition position, const Field& field); | |
| Fragment StoreIndexed(classid_t class_id); | |
| // Takes a [class_id] valid for StoreIndexed. | |
| Fragment StoreIndexedTypedData(classid_t class_id, | |
| intptr_t index_scale, | |
| bool index_unboxed, | |
| AlignmentType alignment = kAlignedAccess); | |
| // Sign-extends kUnboxedInt32 and zero-extends kUnboxedUint32. | |
| Fragment Box(Representation from); | |
| void Push(Definition* definition); | |
| Definition* Peek(intptr_t depth = 0); | |
| Value* Pop(); | |
| Fragment Drop(); | |
| // Drop given number of temps from the stack but preserve top of the stack. | |
| Fragment DropTempsPreserveTop(intptr_t num_temps_to_drop); | |
| Fragment MakeTemp(); | |
| // Create a pseudo-local variable for a location on the expression stack. | |
| // Note: SSA construction currently does not support inserting Phi functions | |
| // for expression stack locations - only real local variables are supported. | |
| // This means that you can't use MakeTemporary in a way that would require | |
| // a Phi in SSA form. For example, the example below will be miscompiled or | |
| // will crash debug VM with assertion when building SSA for optimizing | |
| // compiler: | |
| // | |
| // t = MakeTemporary() | |
| // Branch B1 or B2 | |
| // B1: | |
| // StoreLocal(t, v0) | |
| // goto B3 | |
| // B2: | |
| // StoreLocal(t, v1) | |
| // goto B3 | |
| // B3: | |
| // LoadLocal(t) | |
| LocalVariable* MakeTemporary(const char* suffix = nullptr); | |
| Fragment DropTemporary(LocalVariable** temp); | |
| InputsArray GetArguments(int count); | |
| TargetEntryInstr* BuildTargetEntry(); | |
| TargetEntryInstr* BuildTargetEntry(intptr_t try_index); | |
| FunctionEntryInstr* BuildFunctionEntry(GraphEntryInstr* graph_entry); | |
| JoinEntryInstr* BuildJoinEntry(); | |
| JoinEntryInstr* BuildJoinEntry(intptr_t try_index); | |
| TryEntryInstr* BuildTryEntry(intptr_t try_index); | |
| IndirectEntryInstr* BuildIndirectEntry(intptr_t indirect_id, | |
| intptr_t try_index); | |
| Fragment StrictCompare(TokenPosition position, | |
| Token::Kind kind, | |
| bool number_check = false); | |
| Fragment StrictCompare(Token::Kind kind, bool number_check = false); | |
| Fragment Goto(JoinEntryInstr* destination); | |
| Fragment UnboxedIntConstant(int64_t value, Representation representation); | |
| Fragment IntConstant(int64_t value); | |
| Fragment Constant(const Object& value); | |
| Fragment NullConstant(); | |
| Fragment SmiRelationalOp(Token::Kind kind); | |
| Fragment SmiBinaryOp(Token::Kind op, bool is_truncating = false); | |
| Fragment BinaryIntegerOp(Token::Kind op, | |
| Representation representation, | |
| bool is_truncating = false); | |
| Fragment LoadFpRelativeSlot(intptr_t offset, | |
| CompileType result_type, | |
| Representation representation = kTagged); | |
| Fragment StoreFpRelativeSlot(intptr_t offset); | |
| Fragment BranchIfTrue(TargetEntryInstr** then_entry, | |
| TargetEntryInstr** otherwise_entry, | |
| bool negate = false); | |
| Fragment BranchIfNull(TargetEntryInstr** then_entry, | |
| TargetEntryInstr** otherwise_entry, | |
| bool negate = false); | |
| Fragment BranchIfEqual(TargetEntryInstr** then_entry, | |
| TargetEntryInstr** otherwise_entry, | |
| bool negate = false); | |
| Fragment BranchIfStrictEqual(TargetEntryInstr** then_entry, | |
| TargetEntryInstr** otherwise_entry); | |
| Fragment Return(TokenPosition position); | |
| Fragment Stop(const char* message); | |
| Fragment CheckStackOverflow(TokenPosition position, | |
| intptr_t stack_depth, | |
| intptr_t loop_depth); | |
| Fragment CheckStackOverflowInPrologue(TokenPosition position); | |
| Fragment MemoryCopy(classid_t src_cid, | |
| classid_t dest_cid, | |
| bool unboxed_inputs, | |
| bool can_overlap = true); | |
| Fragment TailCall(const Code& code); | |
| Fragment Utf8Scan(); | |
| intptr_t GetNextDeoptId() { | |
| intptr_t deopt_id = thread_->compiler_state().GetNextDeoptId(); | |
| if (context_level_array_ != nullptr) { | |
| intptr_t level = context_depth_; | |
| context_level_array_->Add(deopt_id); | |
| context_level_array_->Add(level); | |
| } | |
| return deopt_id; | |
| } | |
| intptr_t AllocateTryIndex() { return next_used_try_index_++; } | |
| intptr_t CurrentTryIndex() const { return current_try_index_; } | |
| void SetCurrentTryIndex(intptr_t try_index) { | |
| current_try_index_ = try_index; | |
| } | |
| bool IsCompiledForOsr() { return osr_id_ != DeoptId::kNone; } | |
| bool IsInlining() const { return exit_collector_ != nullptr; } | |
| void InlineBailout(const char* reason); | |
| Fragment LoadArgDescriptor(); | |
| Fragment TestTypeArgsLen(Fragment eq_branch, | |
| Fragment neq_branch, | |
| intptr_t num_type_args); | |
| Fragment TestDelayedTypeArgs(LocalVariable* closure, | |
| Fragment present, | |
| Fragment absent); | |
| Fragment TestAnyTypeArgs(Fragment present, Fragment absent); | |
| JoinEntryInstr* BuildThrowNoSuchMethod(); | |
| Fragment ThrowException(TokenPosition position); | |
| Fragment BooleanNegate(); | |
| Fragment AllocateContext(const ZoneGrowableArray<const Slot*>& scope); | |
| // Top of the stack should be the closure function. | |
| Fragment AllocateClosure(TokenPosition position, | |
| bool has_delayed_type_args, | |
| bool has_instantiator_type_args, | |
| bool has_function_type_args, | |
| bool is_tear_off); | |
| Fragment CreateArray(); | |
| Fragment AllocateRecord(TokenPosition position, RecordShape shape); | |
| Fragment AllocateSmallRecord(TokenPosition position, RecordShape shape); | |
| Fragment AllocateTypedData(TokenPosition position, classid_t class_id); | |
| Fragment InstantiateType(const AbstractType& type); | |
| Fragment InstantiateTypeArguments(const TypeArguments& type_arguments); | |
| Fragment InstantiateDynamicTypeArguments(); | |
| Fragment LoadClassId(); | |
| // Returns true if we are building a graph for inlining of a call site that | |
| // enters the function through the unchecked entry. | |
| bool InliningUncheckedEntry() const { return inlining_unchecked_entry_; } | |
| // Returns depth of expression stack. | |
| intptr_t GetStackDepth() const { | |
| return stack_ == nullptr ? 0 : stack_->definition()->temp_index() + 1; | |
| } | |
| Fragment AllocateObject(TokenPosition position, | |
| const Class& klass, | |
| intptr_t argument_count); | |
| Fragment DebugStepCheck(TokenPosition position); | |
| // Loads 'receiver' and checks it for null. Throws NoSuchMethod if it is null. | |
| // 'function_name' is a selector which is being called (reported in | |
| // NoSuchMethod message). | |
| // Note that this does _not_ use the result of the CheckNullInstr, so it does | |
| // not create a data dependency and might break with code motion. | |
| Fragment CheckNull(TokenPosition position, | |
| LocalVariable* receiver, | |
| const String& function_name); | |
| // Pops the top of the stack, checks it for null, and pushes the result on | |
| // the stack to create a data dependency. | |
| // | |
| // Note that the result can currently only be used in optimized code, because | |
| // optimized code uses FlowGraph::RemoveRedefinitions to remove the | |
| // redefinitions, while unoptimized code does not. | |
| Fragment CheckNullOptimized( | |
| const String& name, | |
| CheckNullInstr::ExceptionType exception_type, | |
| TokenPosition position = TokenPosition::kNoSource); | |
| Fragment CheckNullOptimized( | |
| const String& function_name, | |
| TokenPosition position = TokenPosition::kNoSource) { | |
| return CheckNullOptimized(function_name, CheckNullInstr::kNoSuchMethod, | |
| position); | |
| } | |
| Fragment CheckNotDeeplyImmutable(CheckWritableInstr::Kind kind); | |
| // Records extra unchecked entry point 'unchecked_entry' in 'graph_entry'. | |
| void RecordUncheckedEntryPoint(GraphEntryInstr* graph_entry, | |
| FunctionEntryInstr* unchecked_entry); | |
| // Pop the index of the current entry-point off the stack. If there is any | |
| // entrypoint-tracing hook registered in a pragma for the function, it is | |
| // called with the name of the current function and the current entry-point | |
| // index. | |
| Fragment BuildEntryPointsIntrospection(); | |
| // Builds closure call with given number of arguments. Target closure | |
| // (in bare instructions mode) or closure function (otherwise) is taken from | |
| // top of the stack. | |
| // MoveArgument instructions should be already added for arguments. | |
| Fragment ClosureCall(const Function& target_function, | |
| TokenPosition position, | |
| intptr_t type_args_len, | |
| intptr_t argument_count, | |
| const Array& argument_names, | |
| const InferredTypeMetadata* result_type = nullptr); | |
| // Pops function type arguments, instantiator type arguments, dst_type, and | |
| // value; and type checks value against the type arguments. | |
| Fragment AssertAssignable( | |
| TokenPosition position, | |
| const String& dst_name, | |
| AssertAssignableInstr::Kind kind = AssertAssignableInstr::kUnknown); | |
| // Returns true if we're currently recording deopt_id -> context level | |
| // mapping. | |
| bool is_recording_context_levels() const { | |
| return context_level_array_ != nullptr; | |
| } | |
| // Reset context level for the given deopt id (which was allocated earlier). | |
| void reset_context_depth_for_deopt_id(intptr_t deopt_id); | |
| // Sets raw parameter variables to inferred constant values. | |
| Fragment InitConstantParameters(); | |
| Fragment InvokeMathCFunction(MethodRecognizer::Kind recognized_kind, | |
| intptr_t num_inputs); | |
| // Pops double value and converts it to int as specified | |
| // by the recognized method (kDoubleToInteger, | |
| // kDoubleFloorToInt or kDoubleCeilToInt). | |
| Fragment DoubleToInteger(MethodRecognizer::Kind recognized_kind); | |
| // Pops double value and applies unary math operation. | |
| Fragment UnaryDoubleOp(Token::Kind op); | |
| // Records coverage for this position, if the current VM mode supports it. | |
| Fragment RecordCoverage(TokenPosition position); | |
| Fragment RecordBranchCoverage(TokenPosition position); | |
| // Returns whether this function has a saved arguments descriptor array. | |
| bool has_saved_args_desc_array() { | |
| return function_.HasSavedArgumentsDescriptor(); | |
| } | |
| // Returns the saved arguments descriptor array for functions that have them. | |
| const Array& saved_args_desc_array() { | |
| ASSERT(has_saved_args_desc_array()); | |
| return saved_args_desc_array_; | |
| } | |
| bool optimizing() const { return optimizing_; } | |
| protected: | |
| intptr_t AllocateBlockId() { return ++last_used_block_id_; } | |
| Fragment RecordCoverageImpl(TokenPosition position, bool is_branch_coverage); | |
| intptr_t GetCoverageIndexFor(intptr_t encoded_position); | |
| static bool ShouldOmitCheckBoundsIn(const Function& function, | |
| const Function* caller); | |
| static bool ShouldOmitStackOverflowChecks(bool optimizing, | |
| const Function& function); | |
| bool should_omit_stack_overflow_checks() const { | |
| return should_omit_stack_overflow_checks_; | |
| } | |
| bool should_omit_check_bounds() const { return should_omit_check_bounds_; } | |
| const ParsedFunction* parsed_function_; | |
| const Function& function_; | |
| const bool optimizing_; | |
| const bool should_omit_stack_overflow_checks_; | |
| const bool should_omit_check_bounds_; | |
| Thread* thread_; | |
| Zone* zone_; | |
| intptr_t osr_id_; | |
| // Contains (deopt_id, context_level) pairs. | |
| ZoneGrowableArray<intptr_t>* context_level_array_; | |
| intptr_t context_depth_; | |
| intptr_t last_used_block_id_; | |
| intptr_t current_try_index_; | |
| intptr_t next_used_try_index_; | |
| Value* stack_; | |
| InlineExitCollector* exit_collector_; | |
| const bool inlining_unchecked_entry_; | |
| const Array& saved_args_desc_array_; | |
| // Mapping from token position to the index in the coverage array at which | |
| // coverage state is stored. | |
| IntMap<intptr_t> coverage_state_index_for_position_; | |
| TypedData& coverage_array_; | |
| friend class StreamingFlowGraphBuilder; | |
| private: | |
| DISALLOW_COPY_AND_ASSIGN(BaseFlowGraphBuilder); | |
| }; | |
| } // namespace kernel | |
| } // namespace dart | |
| #endif // RUNTIME_VM_COMPILER_FRONTEND_BASE_FLOW_GRAPH_BUILDER_H_ |