Index: runtime/vm/flow_graph_type_propagator.cc |
diff --git a/runtime/vm/flow_graph_type_propagator.cc b/runtime/vm/flow_graph_type_propagator.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..020172cd19e6581400c8b4c3048c8247e01303aa |
--- /dev/null |
+++ b/runtime/vm/flow_graph_type_propagator.cc |
@@ -0,0 +1,734 @@ |
+// Copyright (c) 2013, 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. |
+ |
+#include "vm/flow_graph_type_propagator.h" |
+ |
+#include "vm/cha.h" |
+#include "vm/bit_vector.h" |
+ |
+namespace dart { |
+ |
+DEFINE_FLAG(bool, trace_type_propagation, false, |
+ "Trace flow graph type propagation"); |
+ |
+DECLARE_FLAG(bool, enable_type_checks); |
+DECLARE_FLAG(bool, use_cha); |
+ |
+ |
+FlowGraphTypePropagator::FlowGraphTypePropagator(FlowGraph* flow_graph) |
+ : FlowGraphVisitor(flow_graph->reverse_postorder()), |
+ flow_graph_(flow_graph), |
+ types_(flow_graph->current_ssa_temp_index()), |
+ in_worklist_(new BitVector(flow_graph->current_ssa_temp_index())) { |
+ for (intptr_t i = 0; i < flow_graph->current_ssa_temp_index(); i++) { |
+ types_.Add(NULL); |
+ } |
+} |
+ |
+ |
+void FlowGraphTypePropagator::Propagate() { |
+ // Walk dominator tree and propagate reaching types to all Values. |
+ // Collect all phis for a fix point iteration. |
+ PropagateRecursive(flow_graph_->graph_entry()); |
+ |
+#ifdef DEBUG |
+ // Initially work-list contains only phis. |
+ for (intptr_t i = 0; i < worklist_.length(); i++) { |
+ ASSERT(worklist_[i]->IsPhi()); |
+ ASSERT(worklist_[i]->Type()->IsNone()); |
+ } |
+#endif |
+ |
+ // Iterate until fix point is reached updating types of definitions. |
+ while (!worklist_.is_empty()) { |
+ Definition* def = RemoveLastFromWorklist(); |
+ if (FLAG_trace_type_propagation) { |
+ OS::Print("recomputing type of v%"Pd": %s\n", |
+ def->ssa_temp_index(), |
+ def->Type()->ToCString()); |
+ } |
+ if (def->RecomputeType()) { |
+ if (FLAG_trace_type_propagation) { |
+ OS::Print(" ... new type %s\n", def->Type()->ToCString()); |
+ } |
+ for (Value::Iterator it(def->input_use_list()); |
+ !it.Done(); |
+ it.Advance()) { |
+ Definition* use_defn = it.Current()->instruction()->AsDefinition(); |
+ if (use_defn != NULL) { |
+ AddToWorklist(use_defn); |
+ } |
+ } |
+ } |
+ } |
+} |
+ |
+ |
+void FlowGraphTypePropagator::PropagateRecursive(BlockEntryInstr* block) { |
+ const intptr_t rollback_point = rollback_.length(); |
+ |
+ block->Accept(this); |
+ |
+ for (ForwardInstructionIterator it(block); !it.Done(); it.Advance()) { |
+ Instruction* instr = it.Current(); |
+ |
+ for (intptr_t i = 0; i < instr->InputCount(); i++) { |
+ VisitValue(instr->InputAt(i)); |
+ } |
+ instr->Accept(this); |
+ } |
+ |
+ GotoInstr* goto_instr = block->last_instruction()->AsGoto(); |
+ if (goto_instr != NULL) { |
+ JoinEntryInstr* join = goto_instr->successor(); |
+ intptr_t pred_index = join->IndexOfPredecessor(block); |
+ ASSERT(pred_index >= 0); |
+ for (PhiIterator it(join); !it.Done(); it.Advance()) { |
+ VisitValue(it.Current()->InputAt(pred_index)); |
+ } |
+ } |
+ |
+ for (intptr_t i = 0; i < block->dominated_blocks().length(); ++i) { |
+ PropagateRecursive(block->dominated_blocks()[i]); |
+ } |
+ |
+ for (intptr_t i = rollback_.length() - 1; i >= rollback_point; i--) { |
+ types_[rollback_[i].index()] = rollback_[i].type(); |
+ } |
+ rollback_.TruncateTo(rollback_point); |
+} |
+ |
+ |
+CompileType* FlowGraphTypePropagator::TypeOf(Definition* def) { |
+ const intptr_t index = def->ssa_temp_index(); |
+ |
+ CompileType* type = types_[index]; |
+ if (type == NULL) { |
+ type = types_[index] = def->Type(); |
+ ASSERT(type != NULL); |
+ } |
+ return type; |
+} |
+ |
+ |
+void FlowGraphTypePropagator::SetTypeOf(Definition* def, CompileType* type) { |
+ const intptr_t index = def->ssa_temp_index(); |
+ rollback_.Add(RollbackEntry(index, types_[index])); |
+ types_[index] = type; |
+} |
+ |
+ |
+void FlowGraphTypePropagator::SetCid(Definition* def, intptr_t cid) { |
+ CompileType* current = TypeOf(def); |
+ if (current->ToCid() == cid) return; |
+ |
+ SetTypeOf(def, CompileType::FromCid(cid)); |
+} |
+ |
+ |
+void FlowGraphTypePropagator::VisitValue(Value* value) { |
+ CompileType* type = TypeOf(value->definition()); |
+ value->SetReachingType(type); |
+ |
+ if (FLAG_trace_type_propagation) { |
+ OS::Print("reaching type to v%"Pd" for v%"Pd" is %s\n", |
+ value->instruction()->IsDefinition() ? |
+ value->instruction()->AsDefinition()->ssa_temp_index() : -1, |
+ value->definition()->ssa_temp_index(), |
+ type->ToCString()); |
+ } |
+} |
+ |
+ |
+void FlowGraphTypePropagator::VisitJoinEntry(JoinEntryInstr* join) { |
+ for (PhiIterator it(join); !it.Done(); it.Advance()) { |
+ if (it.Current()->is_alive()) { |
+ worklist_.Add(it.Current()); |
+ } |
+ } |
+} |
+ |
+ |
+void FlowGraphTypePropagator::VisitCheckSmi(CheckSmiInstr* check) { |
+ SetCid(check->value()->definition(), kSmiCid); |
+} |
+ |
+ |
+void FlowGraphTypePropagator::VisitCheckClass(CheckClassInstr* check) { |
+ if ((check->unary_checks().NumberOfChecks() != 1) || |
+ check->AffectedBySideEffect()) { |
+ // TODO(vegorov): If check is affected by side-effect we can still propagate |
+ // the type further but not the cid. |
+ return; |
+ } |
+ |
+ SetCid(check->value()->definition(), |
+ check->unary_checks().GetReceiverClassIdAt(0)); |
+} |
+ |
+ |
+void FlowGraphTypePropagator::AddToWorklist(Definition* defn) { |
+ if (defn->ssa_temp_index() == -1) { |
+ return; |
+ } |
+ |
+ const intptr_t index = defn->ssa_temp_index(); |
+ if (!in_worklist_->Contains(index)) { |
+ worklist_.Add(defn); |
+ in_worklist_->Add(index); |
+ } |
+} |
+ |
+ |
+Definition* FlowGraphTypePropagator::RemoveLastFromWorklist() { |
+ Definition* defn = worklist_.RemoveLast(); |
+ ASSERT(defn->ssa_temp_index() != -1); |
+ in_worklist_->Remove(defn->ssa_temp_index()); |
+ return defn; |
+} |
+ |
+ |
+void CompileType::Union(CompileType* other) { |
+ if (other->IsNone()) { |
+ return; |
+ } |
+ |
+ if (IsNone()) { |
+ ReplaceWith(other); |
+ return; |
+ } |
+ |
+ is_nullable_ = is_nullable_ || other->is_nullable_; |
+ |
+ if (ToNullableCid() == kNullCid) { |
+ cid_ = other->cid_; |
+ type_ = other->type_; |
+ return; |
+ } |
+ |
+ if (other->ToNullableCid() == kNullCid) { |
+ return; |
+ } |
+ |
+ if (ToNullableCid() != other->ToNullableCid()) { |
+ ASSERT(cid_ != kNullCid); |
+ cid_ = kDynamicCid; |
+ } |
+ |
+ if (ToAbstractType()->IsMoreSpecificThan(*other->ToAbstractType(), NULL)) { |
+ type_ = other->ToAbstractType(); |
+ } else if (ToAbstractType()->IsMoreSpecificThan(*ToAbstractType(), NULL)) { |
+ // Nothing to do. |
+ } else { |
+ // Can't unify. |
+ type_ = &Type::ZoneHandle(Type::DynamicType()); |
+ } |
+} |
+ |
+ |
+static bool IsNullableCid(intptr_t cid) { |
+ ASSERT(cid != kIllegalCid); |
+ return cid == kNullCid || cid == kDynamicCid; |
+} |
+ |
+ |
+CompileType* CompileType::New(intptr_t cid, const AbstractType& type) { |
+ return new CompileType(IsNullableCid(cid), cid, &type); |
+} |
+ |
+ |
+CompileType* CompileType::FromAbstractType(const AbstractType& type, |
+ bool is_nullable) { |
+ return new CompileType(is_nullable, kIllegalCid, &type); |
+} |
+ |
+ |
+CompileType* CompileType::FromCid(intptr_t cid) { |
+ return new CompileType(IsNullableCid(cid), cid, NULL); |
+} |
+ |
+ |
+CompileType* CompileType::Dynamic() { |
+ return New(kDynamicCid, Type::ZoneHandle(Type::DynamicType())); |
+} |
+ |
+ |
+CompileType* CompileType::Null() { |
+ return New(kNullCid, Type::ZoneHandle(Type::NullType())); |
+} |
+ |
+ |
+CompileType* CompileType::Bool() { |
+ return New(kBoolCid, Type::ZoneHandle(Type::BoolType())); |
+} |
+ |
+ |
+CompileType* CompileType::Int() { |
+ return FromAbstractType(Type::ZoneHandle(Type::IntType()), kNonNullable); |
+} |
+ |
+ |
+intptr_t CompileType::ToCid() { |
+ if ((cid_ == kNullCid) || (cid_ == kDynamicCid)) { |
+ return cid_; |
+ } |
+ |
+ return is_nullable_ ? static_cast<intptr_t>(kDynamicCid) : ToNullableCid(); |
+} |
+ |
+ |
+intptr_t CompileType::ToNullableCid() { |
+ if (cid_ == kIllegalCid) { |
+ ASSERT(type_ != NULL); |
+ |
+ if (type_->IsMalformed()) { |
+ cid_ = kDynamicCid; |
+ } else if (type_->IsVoidType()) { |
+ cid_ = kNullCid; |
+ } else if (FLAG_use_cha && type_->HasResolvedTypeClass()) { |
+ const intptr_t cid = Class::Handle(type_->type_class()).id(); |
+ if (!CHA::HasSubclasses(cid)) { |
+ cid_ = cid; |
+ } |
+ } else { |
+ cid_ = kDynamicCid; |
+ } |
+ } |
+ |
+ return cid_; |
+} |
+ |
+ |
+bool CompileType::HasDecidableNullability() { |
+ return !is_nullable_ || IsNull(); |
+} |
+ |
+ |
+bool CompileType::IsNull() { |
+ return (ToCid() == kNullCid); |
+} |
+ |
+ |
+const AbstractType* CompileType::ToAbstractType() { |
+ if (type_ == NULL) { |
+ ASSERT(cid_ != kIllegalCid); |
+ |
+ const Class& type_class = |
+ Class::Handle(Isolate::Current()->class_table()->At(cid_)); |
+ |
+ if (type_class.HasTypeArguments()) { |
+ type_ = &Type::ZoneHandle(Type::DynamicType()); |
+ return type_; |
+ } |
+ |
+ type_ = &Type::ZoneHandle(Type::NewNonParameterizedType(type_class)); |
+ } |
+ |
+ return type_; |
+} |
+ |
+ |
+bool CompileType::CanComputeIsInstanceOf(const AbstractType& type, |
+ bool is_nullable, |
+ bool* is_instance) { |
+ ASSERT(is_instance != NULL); |
+ // We cannot give an answer if the given type is malformed. |
+ if (type.IsMalformed()) { |
+ return false; |
+ } |
+ |
+ if (type.IsDynamicType() || type.IsObjectType()) { |
+ *is_instance = true; |
+ return true; |
+ } |
+ |
+ if (IsNone()) { |
+ return false; |
+ } |
+ |
+ // We should never test for an instance of null. |
+ ASSERT(!type.IsNullType()); |
+ |
+ // Consider the compile type of the value. |
+ const AbstractType& compile_type = *ToAbstractType(); |
+ if (compile_type.IsMalformed()) { |
+ return false; |
+ } |
+ |
+ // If the compile type of the value is void, we are type checking the result |
+ // of a void function, which was checked to be null at the return statement |
+ // inside the function. |
+ if (compile_type.IsVoidType()) { |
+ ASSERT(FLAG_enable_type_checks); |
+ *is_instance = true; |
+ return true; |
+ } |
+ |
+ // The Null type is only a subtype of Object and of dynamic. |
+ // Functions that do not explicitly return a value, implicitly return null, |
+ // except generative constructors, which return the object being constructed. |
+ // It is therefore acceptable for void functions to return null. |
+ if (compile_type.IsNullType()) { |
+ *is_instance = is_nullable || |
+ type.IsObjectType() || type.IsDynamicType() || type.IsVoidType(); |
+ return true; |
+ } |
+ |
+ // A non-null value is not an instance of void. |
+ if (type.IsVoidType()) { |
+ *is_instance = IsNull(); |
+ return HasDecidableNullability(); |
+ } |
+ |
+ // If the value can be null then we can't eliminate the |
+ // check unless null is allowed. |
+ if (is_nullable_ && !is_nullable) { |
+ return false; |
+ } |
+ |
+ Error& malformed_error = Error::Handle(); |
+ *is_instance = compile_type.IsMoreSpecificThan(type, &malformed_error); |
+ return malformed_error.IsNull() && *is_instance; |
+} |
+ |
+ |
+bool CompileType::IsMoreSpecificThan(const AbstractType& other) { |
+ return !IsNone() && ToAbstractType()->IsMoreSpecificThan(other, NULL); |
+} |
+ |
+ |
+CompileType* Value::Type() { |
+ if (reaching_type_ == NULL) { |
+ reaching_type_ = definition()->Type(); |
+ } |
+ return reaching_type_; |
+} |
+ |
+ |
+CompileType* PhiInstr::ComputeInitialType() const { |
+ // Initially type of phis is unknown until type propagation is run |
+ // for the first time. |
+ return CompileType::None(); |
+} |
+ |
+ |
+bool PhiInstr::RecomputeType() { |
+ if (!is_alive()) { |
+ return false; |
+ } |
+ |
+ CompileType* result = CompileType::None(); |
+ |
+ for (intptr_t i = 0; i < InputCount(); i++) { |
+ if (FLAG_trace_type_propagation) { |
+ OS::Print(" phi %"Pd" input %"Pd": v%"Pd" has reaching type %s\n", |
+ ssa_temp_index(), |
+ i, |
+ InputAt(i)->definition()->ssa_temp_index(), |
+ InputAt(i)->Type()->ToCString()); |
+ } |
+ result->Union(InputAt(i)->Type()); |
+ } |
+ |
+ if (result->IsNone()) { |
+ ASSERT(Type()->IsNone()); |
+ return false; |
+ } |
+ |
+ if (Type()->IsNone() || !Type()->IsEqualTo(result)) { |
+ Type()->ReplaceWith(result); |
+ return true; |
+ } |
+ |
+ return false; |
+} |
+ |
+ |
+ |
+CompileType* ParameterInstr::ComputeInitialType() const { |
+ // Note that returning the declared type of the formal parameter would be |
+ // incorrect, because ParameterInstr is used as input to the type check |
+ // verifying the run time type of the passed-in parameter and this check would |
+ // always be wrongly eliminated. |
+ return CompileType::Dynamic(); |
+} |
+ |
+ |
+CompileType* PushArgumentInstr::ComputeInitialType() const { |
+ return CompileType::Dynamic(); |
+} |
+ |
+ |
+CompileType* ConstantInstr::ComputeInitialType() const { |
+ if (value().IsNull()) { |
+ return CompileType::Null(); |
+ } |
+ |
+ if (value().IsInstance()) { |
+ return CompileType::New( |
+ Class::Handle(value().clazz()).id(), |
+ AbstractType::ZoneHandle(Instance::Cast(value()).GetType())); |
+ } else { |
+ ASSERT(value().IsAbstractTypeArguments()); |
+ return CompileType::Dynamic(); |
+ } |
+} |
+ |
+ |
+CompileType* AssertAssignableInstr::ComputeInitialType() const { |
+ CompileType* value_type = value()->Type(); |
+ if (value_type->IsMoreSpecificThan(dst_type())) { |
+ return value_type; |
+ } |
+ return CompileType::FromAbstractType(dst_type()); |
+} |
+ |
+ |
+bool AssertAssignableInstr::RecomputeType() { |
+ CompileType* value_type = value()->Type(); |
+ if (value_type == Type()) { |
+ return false; |
+ } |
+ |
+ if (value_type->IsMoreSpecificThan(dst_type()) && |
+ !Type()->IsEqualTo(value_type)) { |
+ Type()->ReplaceWith(value_type); |
+ return true; |
+ } |
+ |
+ return false; |
+} |
+ |
+ |
+CompileType* AssertBooleanInstr::ComputeInitialType() const { |
+ return CompileType::Bool(); |
+} |
+ |
+ |
+CompileType* ArgumentDefinitionTestInstr::ComputeInitialType() const { |
+ return CompileType::Bool(); |
+} |
+ |
+ |
+CompileType* BooleanNegateInstr::ComputeInitialType() const { |
+ return CompileType::Bool(); |
+} |
+ |
+ |
+CompileType* InstanceOfInstr::ComputeInitialType() const { |
+ return CompileType::Bool(); |
+} |
+ |
+ |
+CompileType* StrictCompareInstr::ComputeInitialType() const { |
+ return CompileType::Bool(); |
+} |
+ |
+ |
+CompileType* EqualityCompareInstr::ComputeInitialType() const { |
+ return IsInlinedNumericComparison() ? CompileType::Bool() |
+ : CompileType::Dynamic(); |
+} |
+ |
+ |
+CompileType* RelationalOpInstr::ComputeInitialType() const { |
+ return IsInlinedNumericComparison() ? CompileType::Bool() |
+ : CompileType::Dynamic(); |
+} |
+ |
+ |
+CompileType* CurrentContextInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kContextCid); |
+} |
+ |
+ |
+CompileType* CloneContextInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kContextCid); |
+} |
+ |
+ |
+CompileType* AllocateContextInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kContextCid); |
+} |
+ |
+ |
+CompileType* StaticCallInstr::ComputeInitialType() const { |
+ if (result_cid_ != kDynamicCid) { |
+ return CompileType::FromCid(result_cid_); |
+ } |
+ |
+ if (FLAG_enable_type_checks) { |
+ return CompileType::FromAbstractType( |
+ AbstractType::ZoneHandle(function().result_type())); |
+ } |
+ |
+ return CompileType::Dynamic(); |
+} |
+ |
+ |
+CompileType* LoadLocalInstr::ComputeInitialType() const { |
+ if (FLAG_enable_type_checks) { |
+ return CompileType::FromAbstractType(local().type()); |
+ } |
+ return CompileType::Dynamic(); |
+} |
+ |
+ |
+CompileType* StoreLocalInstr::ComputeInitialType() const { |
+ // Returns stored value. |
+ return value()->Type(); |
+} |
+ |
+ |
+CompileType* StringFromCharCodeInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(cid_); |
+} |
+ |
+ |
+CompileType* StoreInstanceFieldInstr::ComputeInitialType() const { |
+ return value()->Type(); |
+} |
+ |
+ |
+CompileType* LoadStaticFieldInstr::ComputeInitialType() const { |
+ if (FLAG_enable_type_checks) { |
+ return CompileType::FromAbstractType( |
+ AbstractType::ZoneHandle(field().type())); |
+ } |
+ return CompileType::Dynamic(); |
+} |
+ |
+ |
+CompileType* StoreStaticFieldInstr::ComputeInitialType() const { |
+ return value()->Type(); |
+} |
+ |
+ |
+CompileType* CreateArrayInstr::ComputeInitialType() const { |
+ return CompileType::FromAbstractType(type(), CompileType::kNonNullable); |
+} |
+ |
+ |
+CompileType* CreateClosureInstr::ComputeInitialType() const { |
+ const Function& fun = function(); |
+ const Class& signature_class = Class::Handle(fun.signature_class()); |
+ return CompileType::FromAbstractType( |
+ Type::ZoneHandle(signature_class.SignatureType()), |
+ CompileType::kNonNullable); |
+} |
+ |
+ |
+CompileType* AllocateObjectInstr::ComputeInitialType() const { |
+ // TODO(vegorov): Incorporate type arguments into the returned type. |
+ return CompileType::FromCid(cid_); |
+} |
+ |
+ |
+CompileType* LoadFieldInstr::ComputeInitialType() const { |
+ // Type may be null if the field is a VM field, e.g. context parent. |
+ // Keep it as null for debug purposes and do not return dynamic in production |
+ // mode, since misuse of the type would remain undetected. |
+ if (type().IsNull()) { |
+ return CompileType::Dynamic(); |
+ } |
+ |
+ if (FLAG_enable_type_checks) { |
+ return CompileType::FromAbstractType(type()); |
+ } |
+ |
+ return CompileType::FromCid(result_cid_); |
+} |
+ |
+ |
+CompileType* StoreVMFieldInstr::ComputeInitialType() const { |
+ return value()->Type(); |
+} |
+ |
+ |
+CompileType* BinarySmiOpInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kSmiCid); |
+} |
+ |
+ |
+CompileType* UnarySmiOpInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kSmiCid); |
+} |
+ |
+ |
+CompileType* DoubleToSmiInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kSmiCid); |
+} |
+ |
+ |
+CompileType* ConstraintInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kSmiCid); |
+} |
+ |
+ |
+CompileType* BinaryMintOpInstr::ComputeInitialType() const { |
+ return CompileType::Int(); |
+} |
+ |
+ |
+CompileType* ShiftMintOpInstr::ComputeInitialType() const { |
+ return CompileType::Int(); |
+} |
+ |
+ |
+CompileType* UnaryMintOpInstr::ComputeInitialType() const { |
+ return CompileType::Int(); |
+} |
+ |
+ |
+CompileType* BoxIntegerInstr::ComputeInitialType() const { |
+ return CompileType::Int(); |
+} |
+ |
+ |
+CompileType* UnboxIntegerInstr::ComputeInitialType() const { |
+ return CompileType::Int(); |
+} |
+ |
+ |
+CompileType* DoubleToIntegerInstr::ComputeInitialType() const { |
+ return CompileType::Int(); |
+} |
+ |
+ |
+CompileType* BinaryDoubleOpInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kDoubleCid); |
+} |
+ |
+ |
+CompileType* MathSqrtInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kDoubleCid); |
+} |
+ |
+ |
+CompileType* UnboxDoubleInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kDoubleCid); |
+} |
+ |
+ |
+CompileType* BoxDoubleInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kDoubleCid); |
+} |
+ |
+ |
+CompileType* SmiToDoubleInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kDoubleCid); |
+} |
+ |
+ |
+CompileType* DoubleToDoubleInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kDoubleCid); |
+} |
+ |
+ |
+CompileType* InvokeMathCFunctionInstr::ComputeInitialType() const { |
+ return CompileType::FromCid(kDoubleCid); |
+} |
+ |
+ |
+} // namespace dart |