Commit 8ada7538 authored by Caitlin Potter's avatar Caitlin Potter Committed by Commit Bot

Reland "[builtins] port Promise.all to CSA"

Simplifies the implementation of IteratorClose in IteratorBuiltinsAssembler, and makes clear that it is only invoked when an exception occurs. Adds exception handling support to GetIterator, IteratorStep, and IteratorCloseOnException.

Moves the Promise.all resolveElement closure and it's caller to
builtins-promise-gen.cc.

Instead of creating an internal array (and copying its elements into a
result
array), a single JSArray is allocated, and appended with
BuildAppendJSArray(),
falling back to %CreateDataProperty(), and elements are updated in the
resolve
closure the same way. This should always be unobservable.

This CL increases the size of snapshot_blob.bin on an x64.release build
by 8.51kb

BUG=v8:5343
R=cbruni@chromium.org, gsathysa@chromium.org, jgruber@chromium.org, hpayer@chromium.org, tebbi@chromium.org

Change-Id: I29c4a529154ef49ad65555ce6ddc2c5b7c9de6b3
Reviewed-on: https://chromium-review.googlesource.com/508473
Commit-Queue: Caitlin Potter <caitp@igalia.com>
Reviewed-by: 's avatarHannes Payer <hpayer@chromium.org>
Reviewed-by: 's avatarTobias Tebbi <tebbi@chromium.org>
Reviewed-by: 's avatarCamillo Bruni <cbruni@chromium.org>
Reviewed-by: 's avatarSathya Gunasekaran <gsathya@chromium.org>
Reviewed-by: 's avatarJakob Gruber <jgruber@chromium.org>
Cr-Commit-Position: refs/heads/master@{#45946}
parent b267efc7
......@@ -2140,6 +2140,9 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
InstallSpeciesGetter(promise_fun);
SimpleInstallFunction(promise_fun, "all", Builtins::kPromiseAll, 1, true,
DONT_ENUM);
SimpleInstallFunction(promise_fun, "resolve", Builtins::kPromiseResolve, 1,
true, DONT_ENUM);
......@@ -2222,6 +2225,16 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
info->set_length(1);
native_context()->set_promise_reject_shared_fun(*info);
}
{
Handle<Code> code =
isolate->builtins()->PromiseAllResolveElementClosure();
Handle<SharedFunctionInfo> info =
factory->NewSharedFunctionInfo(factory->empty_string(), code, false);
info->set_internal_formal_parameter_count(1);
info->set_length(1);
native_context()->set_promise_all_resolve_element_shared_fun(*info);
}
}
{ // -- R e g E x p
......
......@@ -159,7 +159,7 @@ TF_BUILTIN(MapConstructor, CollectionsBuiltinsAssembler) {
Node* const fast_iterator_result_map =
LoadContextElement(native_context, Context::ITERATOR_RESULT_MAP_INDEX);
VARIABLE(var_exception, MachineRepresentation::kTagged, UndefinedConstant());
VARIABLE(var_exception, MachineRepresentation::kTagged, TheHoleConstant());
Label loop(this), if_notobject(this), if_exception(this);
Goto(&loop);
......@@ -199,7 +199,8 @@ TF_BUILTIN(MapConstructor, CollectionsBuiltinsAssembler) {
BIND(&if_exception);
{
iterator_assembler.IteratorClose(context, iterator, var_exception.value());
iterator_assembler.IteratorCloseOnException(context, iterator,
&var_exception);
}
BIND(&if_notcallable);
......@@ -285,7 +286,7 @@ TF_BUILTIN(SetConstructor, CollectionsBuiltinsAssembler) {
Node* const fast_iterator_result_map =
LoadContextElement(native_context, Context::ITERATOR_RESULT_MAP_INDEX);
VARIABLE(var_exception, MachineRepresentation::kTagged, UndefinedConstant());
VARIABLE(var_exception, MachineRepresentation::kTagged, TheHoleConstant());
Label loop(this), if_notobject(this), if_exception(this);
Goto(&loop);
......@@ -307,7 +308,8 @@ TF_BUILTIN(SetConstructor, CollectionsBuiltinsAssembler) {
BIND(&if_exception);
{
iterator_assembler.IteratorClose(context, iterator, var_exception.value());
iterator_assembler.IteratorCloseOnException(context, iterator,
&var_exception);
}
BIND(&if_notcallable);
......
......@@ -772,6 +772,7 @@ namespace internal {
TFJ(PromiseResolveClosure, 1, kValue) \
/* ES #sec-promise-reject-functions */ \
TFJ(PromiseRejectClosure, 1, kValue) \
TFJ(PromiseAllResolveElementClosure, 1, kValue) \
/* ES #sec-promise.prototype.then */ \
TFJ(PromiseThen, 2, kOnFullfilled, kOnRejected) \
/* ES #sec-promise.prototype.catch */ \
......@@ -791,6 +792,8 @@ namespace internal {
TFJ(PromiseCatchFinally, 1, kReason) \
TFJ(PromiseValueThunkFinally, 0) \
TFJ(PromiseThrowerFinally, 0) \
/* ES #sec-promise.all */ \
TFJ(PromiseAll, 1, kIterable) \
\
/* Proxy */ \
CPP(ProxyConstructor) \
......@@ -1098,6 +1101,7 @@ namespace internal {
V(AsyncGeneratorAwaitCaught) \
V(AsyncGeneratorAwaitUncaught) \
V(PerformNativePromiseThen) \
V(PromiseAll) \
V(PromiseConstructor) \
V(PromiseHandle) \
V(PromiseResolve) \
......
......@@ -9,11 +9,15 @@ namespace internal {
using compiler::Node;
Node* IteratorBuiltinsAssembler::GetIterator(Node* context, Node* object) {
Node* IteratorBuiltinsAssembler::GetIterator(Node* context, Node* object,
Label* if_exception,
Variable* exception) {
Node* method = GetProperty(context, object, factory()->iterator_symbol());
GotoIfException(method, if_exception, exception);
Callable callable = CodeFactory::Call(isolate());
Node* iterator = CallJS(callable, context, method, object);
GotoIfException(iterator, if_exception, exception);
Label done(this), if_notobject(this, Label::kDeferred);
GotoIf(TaggedIsSmi(iterator), &if_notobject);
......@@ -21,8 +25,10 @@ Node* IteratorBuiltinsAssembler::GetIterator(Node* context, Node* object) {
BIND(&if_notobject);
{
Node* ret =
CallRuntime(Runtime::kThrowTypeError, context,
SmiConstant(MessageTemplate::kNotAnIterator), iterator);
GotoIfException(ret, if_exception, exception);
Unreachable();
}
......@@ -32,25 +38,31 @@ Node* IteratorBuiltinsAssembler::GetIterator(Node* context, Node* object) {
Node* IteratorBuiltinsAssembler::IteratorStep(Node* context, Node* iterator,
Label* if_done,
Node* fast_iterator_result_map) {
Node* fast_iterator_result_map,
Label* if_exception,
Variable* exception) {
DCHECK_NOT_NULL(if_done);
// IteratorNext
Node* next_method = GetProperty(context, iterator, factory()->next_string());
GotoIfException(next_method, if_exception, exception);
// 1. a. Let result be ? Invoke(iterator, "next", « »).
Callable callable = CodeFactory::Call(isolate());
Node* result = CallJS(callable, context, next_method, iterator);
GotoIfException(result, if_exception, exception);
// 3. If Type(result) is not Object, throw a TypeError exception.
Label if_notobject(this, Label::kDeferred), return_result(this);
GotoIf(TaggedIsSmi(result), &if_notobject);
GotoIfNot(IsJSReceiver(result), &if_notobject);
Label if_generic(this);
VARIABLE(var_done, MachineRepresentation::kTagged);
if (fast_iterator_result_map != nullptr) {
// Fast iterator result case:
Label if_generic(this);
// 4. Return result.
Node* map = LoadMap(result);
GotoIfNot(WordEqual(map, fast_iterator_result_map), &if_generic);
......@@ -61,15 +73,16 @@ Node* IteratorBuiltinsAssembler::IteratorStep(Node* context, Node* iterator,
CSA_ASSERT(this, IsBoolean(done));
var_done.Bind(done);
Goto(&return_result);
} else {
Goto(&if_generic);
}
BIND(&if_generic);
}
// Generic iterator result case:
{
// IteratorComplete
// 2. Return ToBoolean(? Get(iterResult, "done")).
Node* done = GetProperty(context, result, factory()->done_string());
GotoIfException(done, if_exception, exception);
var_done.Bind(done);
Label to_boolean(this, Label::kDeferred);
......@@ -83,8 +96,10 @@ Node* IteratorBuiltinsAssembler::IteratorStep(Node* context, Node* iterator,
BIND(&if_notobject);
{
Node* ret =
CallRuntime(Runtime::kThrowIteratorResultNotAnObject, context, result);
Goto(if_done);
GotoIfException(ret, if_exception, exception);
Unreachable();
}
BIND(&return_result);
......@@ -93,23 +108,28 @@ Node* IteratorBuiltinsAssembler::IteratorStep(Node* context, Node* iterator,
}
Node* IteratorBuiltinsAssembler::IteratorValue(Node* context, Node* result,
Node* fast_iterator_result_map) {
Node* fast_iterator_result_map,
Label* if_exception,
Variable* exception) {
CSA_ASSERT(this, IsJSReceiver(result));
Label exit(this), if_generic(this);
Label exit(this);
VARIABLE(var_value, MachineRepresentation::kTagged);
if (fast_iterator_result_map != nullptr) {
// Fast iterator result case:
Label if_generic(this);
Node* map = LoadMap(result);
GotoIfNot(WordEqual(map, fast_iterator_result_map), &if_generic);
var_value.Bind(LoadObjectField(result, JSIteratorResult::kValueOffset));
Goto(&exit);
} else {
Goto(&if_generic);
}
BIND(&if_generic);
}
// Generic iterator result case:
{
Node* value = GetProperty(context, result, factory()->value_string());
GotoIfException(value, if_exception, exception);
var_value.Bind(value);
Goto(&exit);
}
......@@ -118,46 +138,46 @@ Node* IteratorBuiltinsAssembler::IteratorValue(Node* context, Node* result,
return var_value.value();
}
void IteratorBuiltinsAssembler::IteratorClose(Node* context, Node* iterator,
Node* exception) {
void IteratorBuiltinsAssembler::IteratorCloseOnException(Node* context,
Node* iterator,
Label* if_exception,
Variable* exception) {
// Perform ES #sec-iteratorclose when an exception occurs. This simpler
// algorithm does not include redundant steps which are never reachable from
// the spec IteratorClose algorithm.
DCHECK_NOT_NULL(if_exception);
DCHECK_NOT_NULL(exception);
CSA_ASSERT(this, IsNotTheHole(exception->value()));
CSA_ASSERT(this, IsJSReceiver(iterator));
VARIABLE(var_iter_exception, MachineRepresentation::kTagged,
UndefinedConstant());
Label rethrow_exception(this);
// Let return be ? GetMethod(iterator, "return").
Node* method = GetProperty(context, iterator, factory()->return_string());
GotoIf(Word32Or(IsUndefined(method), IsNull(method)), &rethrow_exception);
GotoIfException(method, if_exception, exception);
Label if_iter_exception(this), if_notobject(this);
// If return is undefined, return Completion(completion).
GotoIf(Word32Or(IsUndefined(method), IsNull(method)), if_exception);
{
// Let innerResult be Call(return, iterator, « »).
// If an exception occurs, the original exception remains bound
Node* inner_result =
CallJS(CodeFactory::Call(isolate()), context, method, iterator);
GotoIfException(inner_result, if_exception, nullptr);
GotoIfException(inner_result, &if_iter_exception, &var_iter_exception);
GotoIfNot(IsUndefined(exception), &rethrow_exception);
GotoIf(TaggedIsSmi(inner_result), &if_notobject);
Branch(IsJSReceiver(inner_result), &rethrow_exception, &if_notobject);
BIND(&if_notobject);
{
CallRuntime(Runtime::kThrowIteratorResultNotAnObject, context,
inner_result);
Unreachable();
// (If completion.[[Type]] is throw) return Completion(completion).
Goto(if_exception);
}
}
BIND(&if_iter_exception);
{
GotoIfNot(IsUndefined(exception), &rethrow_exception);
CallRuntime(Runtime::kReThrow, context, var_iter_exception.value());
Unreachable();
}
void IteratorBuiltinsAssembler::IteratorCloseOnException(Node* context,
Node* iterator,
Variable* exception) {
Label rethrow(this, Label::kDeferred);
IteratorCloseOnException(context, iterator, &rethrow, exception);
BIND(&rethrow_exception);
{
CallRuntime(Runtime::kReThrow, context, exception);
BIND(&rethrow);
CallRuntime(Runtime::kReThrow, context, exception->value());
Unreachable();
}
}
} // namespace internal
......
......@@ -16,7 +16,8 @@ class IteratorBuiltinsAssembler : public CodeStubAssembler {
// https://tc39.github.io/ecma262/#sec-getiterator --- never used for
// @@asyncIterator.
Node* GetIterator(Node* context, Node* object);
Node* GetIterator(Node* context, Node* object, Label* if_exception = nullptr,
Variable* exception = nullptr);
// https://tc39.github.io/ecma262/#sec-iteratorstep
// Returns `false` if the iterator is done, otherwise returns an
......@@ -24,17 +25,24 @@ class IteratorBuiltinsAssembler : public CodeStubAssembler {
// `fast_iterator_result_map` refers to the map for the JSIteratorResult
// object, loaded from the native context.
Node* IteratorStep(Node* context, Node* iterator, Label* if_done,
Node* fast_iterator_result_map = nullptr);
Node* fast_iterator_result_map = nullptr,
Label* if_exception = nullptr,
Variable* exception = nullptr);
// https://tc39.github.io/ecma262/#sec-iteratorvalue
// Return the `value` field from an iterator.
// `fast_iterator_result_map` refers to the map for the JSIteratorResult
// object, loaded from the native context.
Node* IteratorValue(Node* context, Node* result,
Node* fast_iterator_result_map = nullptr);
Node* fast_iterator_result_map = nullptr,
Label* if_exception = nullptr,
Variable* exception = nullptr);
// https://tc39.github.io/ecma262/#sec-iteratorclose
void IteratorClose(Node* context, Node* iterator, Node* exception);
void IteratorCloseOnException(Node* context, Node* iterator,
Label* if_exception, Variable* exception);
void IteratorCloseOnException(Node* context, Node* iterator,
Variable* exception);
};
} // namespace internal
......
This diff is collapsed.
......@@ -28,6 +28,27 @@ class PromiseBuiltinsAssembler : public CodeStubAssembler {
kPromiseContextLength,
};
protected:
enum PromiseAllResolveElementContextSlots {
// Whether the resolve callback was already called.
kPromiseAllResolveElementAlreadyVisitedSlot = Context::MIN_CONTEXT_SLOTS,
// Index into the values array
kPromiseAllResolveElementIndexSlot,
// Remaining elements count (mutable HeapNumber)
kPromiseAllResolveElementRemainingElementsSlot,
// Promise capability from Promise.all
kPromiseAllResolveElementCapabilitySlot,
// Values array from Promise.all
kPromiseAllResolveElementValuesArraySlot,
kPromiseAllResolveElementLength
};
public:
enum FunctionContextSlot {
kCapabilitySlot = Context::MIN_CONTEXT_SLOTS,
......@@ -135,6 +156,13 @@ class PromiseBuiltinsAssembler : public CodeStubAssembler {
Node* CreateThrowerFunctionContext(Node* reason, Node* native_context);
Node* CreateThrowerFunction(Node* reason, Node* native_context);
Node* PerformPromiseAll(Node* context, Node* constructor, Node* capability,
Node* iterator, Label* if_exception,
Variable* var_exception);
Node* IncrementSmiCell(Node* cell, Label* if_overflow = nullptr);
Node* DecrementSmiCell(Node* cell);
private:
Node* AllocateJSPromise(Node* context);
};
......
......@@ -156,6 +156,9 @@ HEAP_CONSTANT_LIST(HEAP_CONSTANT_ACCESSOR);
#define HEAP_CONSTANT_TEST(rootName, name) \
Node* CodeStubAssembler::Is##name(Node* value) { \
return WordEqual(value, name##Constant()); \
} \
Node* CodeStubAssembler::IsNot##name(Node* value) { \
return WordNotEqual(value, name##Constant()); \
}
HEAP_CONSTANT_LIST(HEAP_CONSTANT_TEST);
#undef HEAP_CONSTANT_TEST
......@@ -1704,6 +1707,31 @@ void CodeStubAssembler::BuildAppendJSArray(ElementsKind kind, Node* array,
StoreObjectFieldNoWriteBarrier(array, JSArray::kLengthOffset, length);
}
Node* CodeStubAssembler::AllocateCellWithValue(Node* value,
WriteBarrierMode mode) {
Node* result = Allocate(Cell::kSize, kNone);
StoreMapNoWriteBarrier(result, Heap::kCellMapRootIndex);
StoreCellValue(result, value, mode);
return result;
}
Node* CodeStubAssembler::LoadCellValue(Node* cell) {
CSA_SLOW_ASSERT(this, HasInstanceType(cell, CELL_TYPE));
return LoadObjectField(cell, Cell::kValueOffset);
}
Node* CodeStubAssembler::StoreCellValue(Node* cell, Node* value,
WriteBarrierMode mode) {
CSA_SLOW_ASSERT(this, HasInstanceType(cell, CELL_TYPE));
DCHECK(mode == SKIP_WRITE_BARRIER || mode == UPDATE_WRITE_BARRIER);
if (mode == UPDATE_WRITE_BARRIER) {
return StoreObjectField(cell, Cell::kValueOffset, value);
} else {
return StoreObjectFieldNoWriteBarrier(cell, Cell::kValueOffset, value);
}
}
Node* CodeStubAssembler::AllocateHeapNumber(MutableMode mode) {
Node* result = Allocate(HeapNumber::kSize, kNone);
Heap::RootListIndex heap_map_index =
......
......@@ -152,7 +152,9 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler {
HEAP_CONSTANT_LIST(HEAP_CONSTANT_ACCESSOR)
#undef HEAP_CONSTANT_ACCESSOR
#define HEAP_CONSTANT_TEST(rootName, name) Node* Is##name(Node* value);
#define HEAP_CONSTANT_TEST(rootName, name) \
Node* Is##name(Node* value); \
Node* IsNot##name(Node* value);
HEAP_CONSTANT_LIST(HEAP_CONSTANT_TEST)
#undef HEAP_CONSTANT_TEST
......@@ -547,6 +549,17 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler {
void StoreFieldsNoWriteBarrier(Node* start_address, Node* end_address,
Node* value);
Node* AllocateCellWithValue(Node* value,
WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
Node* AllocateSmiCell(int value = 0) {
return AllocateCellWithValue(SmiConstant(value), SKIP_WRITE_BARRIER);
}
Node* LoadCellValue(Node* cell);
Node* StoreCellValue(Node* cell, Node* value,
WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
// Allocate a HeapNumber without initializing its value.
Node* AllocateHeapNumber(MutableMode mode = IMMUTABLE);
// Allocate a HeapNumber with a specific value.
......@@ -1452,6 +1465,11 @@ class V8_EXPORT_PRIVATE CodeStubAssembler : public compiler::CodeAssembler {
UndefinedConstant(), SmiConstant(message), args...);
}
void Abort(BailoutReason reason) {
CallRuntime(Runtime::kAbort, NoContextConstant(), SmiConstant(reason));
Unreachable();
}
protected:
void DescriptorLookup(Node* unique_name, Node* descriptors, Node* bitfield3,
Label* if_found, Variable* var_name_index,
......
......@@ -554,10 +554,16 @@ Node* CodeAssembler::Projection(int index, Node* value) {
void CodeAssembler::GotoIfException(Node* node, Label* if_exception,
Variable* exception_var) {
DCHECK(!node->op()->HasProperty(Operator::kNoThrow));
if (if_exception == nullptr) {
// If no handler is supplied, don't add continuations
return;
}
Label success(this), exception(this, Label::kDeferred);
success.MergeVariables();
exception.MergeVariables();
DCHECK(!node->op()->HasProperty(Operator::kNoThrow));
raw_assembler()->Continuations(node, success.label_, exception.label_);
......
......@@ -321,6 +321,8 @@ enum ContextLookupFlags {
promise_value_thunk_finally_shared_fun) \
V(PROMISE_THROWER_FINALLY_SHARED_FUN, SharedFunctionInfo, \
promise_thrower_finally_shared_fun) \
V(PROMISE_ALL_RESOLVE_ELEMENT_SHARED_FUN, SharedFunctionInfo, \
promise_all_resolve_element_shared_fun) \
V(PROMISE_PROTOTYPE_MAP_INDEX, Map, promise_prototype_map) \
V(REGEXP_EXEC_FUNCTION_INDEX, JSFunction, regexp_exec_function) \
V(REGEXP_FUNCTION_INDEX, JSFunction, regexp_function) \
......
......@@ -40,6 +40,10 @@ void StaticNewSpaceVisitor<StaticVisitor>::Initialize() {
&FixedBodyVisitor<StaticVisitor, SlicedString::BodyDescriptor,
int>::Visit);
table_.Register(
kVisitCell,
&FixedBodyVisitor<StaticVisitor, Cell::BodyDescriptor, int>::Visit);
table_.Register(
kVisitSymbol,
&FixedBodyVisitor<StaticVisitor, Symbol::BodyDescriptor, int>::Visit);
......
......@@ -32,6 +32,10 @@ class ScavengingVisitor : public StaticVisitorBase {
table_.Register(kVisitSeqOneByteString, &EvacuateSeqOneByteString);
table_.Register(kVisitSeqTwoByteString, &EvacuateSeqTwoByteString);
table_.Register(kVisitShortcutCandidate, &EvacuateShortcutCandidate);
table_.Register(
kVisitCell,
&ObjectEvacuationStrategy<POINTER_OBJECT>::template VisitSpecialized<
Cell::kSize>);
table_.Register(kVisitThinString, &EvacuateThinString);
table_.Register(kVisitByteArray, &EvacuateByteArray);
table_.Register(kVisitFixedArray, &EvacuateFixedArray);
......
......@@ -23,70 +23,6 @@ var GlobalPromise = global.Promise;
// Combinators.
// ES#sec-promise.all
// Promise.all ( iterable )
function PromiseAll(iterable) {
if (!IS_RECEIVER(this)) {
throw %make_type_error(kCalledOnNonObject, "Promise.all");
}
// false debugEvent so that forwarding the rejection through all does not
// trigger redundant ExceptionEvents
var deferred = %new_promise_capability(this, false);
var resolutions = new InternalArray();
var count;
// For catch prediction, don't treat the .then calls as handling it;
// instead, recurse outwards.
var instrumenting = DEBUG_IS_ACTIVE;
if (instrumenting) {
SET_PRIVATE(deferred.reject, promiseForwardingHandlerSymbol, true);
}
function CreateResolveElementFunction(index, values, promiseCapability) {
var alreadyCalled = false;
return (x) => {
if (alreadyCalled === true) return;
alreadyCalled = true;
values[index] = x;
if (--count === 0) {
var valuesArray = [];
%MoveArrayContents(values, valuesArray);
%_Call(promiseCapability.resolve, UNDEFINED, valuesArray);
}
};
}
try {
var i = 0;
count = 1;
for (var value of iterable) {
var nextPromise = this.resolve(value);
++count;
var throwawayPromise = nextPromise.then(
CreateResolveElementFunction(i, resolutions, deferred),
deferred.reject);
// For catch prediction, mark that rejections here are semantically
// handled by the combined Promise.
if (instrumenting && %is_promise(throwawayPromise)) {
SET_PRIVATE(throwawayPromise, promiseHandledBySymbol, deferred.promise);
}
++i;
}
// 6.d
if (--count === 0) {
var valuesArray = [];
%MoveArrayContents(resolutions, valuesArray);
%_Call(deferred.resolve, UNDEFINED, valuesArray);
}
} catch (e) {
%_Call(deferred.reject, UNDEFINED, e);
}
return deferred.promise;
}
// ES#sec-promise.race
// Promise.race ( iterable )
function PromiseRace(iterable) {
......@@ -125,7 +61,6 @@ function PromiseRace(iterable) {
// Install exported functions.
utils.InstallFunctions(GlobalPromise, DONT_ENUM, [
"all", PromiseAll,
"race", PromiseRace,
]);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment