Commit 823682ea authored by verwaest's avatar verwaest Committed by Commit bot

Use LookupIterator for GetElementAttributes and friends

BUG=v8:4137
LOG=n

Review URL: https://codereview.chromium.org/1153583006

Cr-Commit-Position: refs/heads/master@{#28757}
parent 341e4ac2
......@@ -3565,7 +3565,7 @@ Maybe<bool> v8::Object::CreateDataProperty(v8::Local<v8::Context> context,
}
Maybe<PropertyAttributes> attributes =
i::JSReceiver::GetOwnElementAttribute(self, index);
i::JSReceiver::GetOwnElementAttributes(self, index);
if (attributes.IsJust() && attributes.FromJust() & DONT_DELETE) {
return Just(false);
}
......
......@@ -59,16 +59,23 @@ LookupIterator::State LookupIterator::LookupInHolder(Map* const map,
case INTERCEPTOR:
if (IsElement()) {
// TODO(verwaest): Optimize.
JSObject* js_object = JSObject::cast(holder);
ElementsAccessor* accessor = js_object->GetElementsAccessor();
FixedArrayBase* backing_store = js_object->elements();
number_ = accessor->GetIndexForKey(backing_store, index_);
if (number_ == kMaxUInt32) return NOT_FOUND;
if (accessor->GetAttributes(js_object, index_, backing_store) ==
ABSENT) {
return NOT_FOUND;
if (holder->IsStringObjectWithCharacterAt(index_)) {
PropertyAttributes attributes =
static_cast<PropertyAttributes>(READ_ONLY | DONT_DELETE);
property_details_ = PropertyDetails(attributes, v8::internal::DATA, 0,
PropertyCellType::kNoCell);
} else {
JSObject* js_object = JSObject::cast(holder);
ElementsAccessor* accessor = js_object->GetElementsAccessor();
FixedArrayBase* backing_store = js_object->elements();
number_ = accessor->GetIndexForKey(backing_store, index_);
if (number_ == kMaxUInt32) return NOT_FOUND;
if (accessor->GetAttributes(js_object, index_, backing_store) ==
ABSENT) {
return NOT_FOUND;
}
property_details_ = accessor->GetDetails(backing_store, number_);
}
property_details_ = accessor->GetDetails(backing_store, number_);
} else if (holder->IsGlobalObject()) {
GlobalDictionary* dict = JSObject::cast(holder)->global_dictionary();
int number = dict->FindEntry(name_);
......
......@@ -264,6 +264,13 @@ Handle<Object> LookupIterator::FetchValue() const {
Handle<JSObject> holder = GetHolder<JSObject>();
if (IsElement()) {
// TODO(verwaest): Optimize.
if (holder->IsStringObjectWithCharacterAt(index_)) {
Handle<JSValue> js_value = Handle<JSValue>::cast(holder);
Handle<String> string(String::cast(js_value->value()));
return factory()->LookupSingleCharacterStringFromCode(
String::Flatten(string)->Get(index_));
}
ElementsAccessor* accessor = holder->GetElementsAccessor();
return accessor->Get(holder, index_);
} else if (holder_map_->IsGlobalObjectMap()) {
......
......@@ -1189,15 +1189,6 @@ Handle<Object> Object::GetPrototypeSkipHiddenPrototypes(
}
MaybeHandle<Object> Object::GetPropertyOrElement(Handle<Object> object,
Handle<Name> name) {
uint32_t index;
Isolate* isolate = name->GetIsolate();
if (name->AsArrayIndex(&index)) return GetElement(isolate, object, index);
return GetProperty(object, name);
}
MaybeHandle<Object> Object::GetProperty(Isolate* isolate,
Handle<Object> object,
const char* name) {
......@@ -6962,12 +6953,24 @@ String* String::GetForwardedInternalizedString() {
}
MaybeHandle<Object> Object::GetPropertyOrElement(Handle<Object> object,
Handle<Name> name) {
uint32_t index;
LookupIterator it = name->AsArrayIndex(&index)
? LookupIterator(name->GetIsolate(), object, index)
: LookupIterator(object, name);
return GetProperty(&it);
}
Maybe<bool> JSReceiver::HasProperty(Handle<JSReceiver> object,
Handle<Name> name) {
// Call the "has" trap on proxies.
if (object->IsJSProxy()) {
Handle<JSProxy> proxy = Handle<JSProxy>::cast(object);
return JSProxy::HasPropertyWithHandler(proxy, name);
}
Maybe<PropertyAttributes> result = GetPropertyAttributes(object, name);
return result.IsJust() ? Just(result.FromJust() != ABSENT) : Nothing<bool>();
}
......@@ -6975,34 +6978,80 @@ Maybe<bool> JSReceiver::HasProperty(Handle<JSReceiver> object,
Maybe<bool> JSReceiver::HasOwnProperty(Handle<JSReceiver> object,
Handle<Name> name) {
// Call the "has" trap on proxies.
if (object->IsJSProxy()) {
Handle<JSProxy> proxy = Handle<JSProxy>::cast(object);
return JSProxy::HasPropertyWithHandler(proxy, name);
}
Maybe<PropertyAttributes> result = GetOwnPropertyAttributes(object, name);
return result.IsJust() ? Just(result.FromJust() != ABSENT) : Nothing<bool>();
}
Maybe<PropertyAttributes> JSReceiver::GetPropertyAttributes(
Handle<JSReceiver> object, Handle<Name> key) {
uint32_t index;
if (object->IsJSObject() && key->AsArrayIndex(&index)) {
return GetElementAttribute(object, index);
}
LookupIterator it(object, key);
Handle<JSReceiver> object, Handle<Name> name) {
uint32_t index = 0;
LookupIterator it = name->AsArrayIndex(&index)
? LookupIterator(name->GetIsolate(), object, index)
: LookupIterator(object, name);
return GetPropertyAttributes(&it);
}
Maybe<PropertyAttributes> JSReceiver::GetElementAttribute(
Handle<JSReceiver> object, uint32_t index) {
Maybe<PropertyAttributes> JSReceiver::GetOwnPropertyAttributes(
Handle<JSReceiver> object, Handle<Name> name) {
uint32_t index = 0;
LookupIterator::Configuration c = LookupIterator::HIDDEN;
LookupIterator it = name->AsArrayIndex(&index)
? LookupIterator(name->GetIsolate(), object, index, c)
: LookupIterator(object, name, c);
return GetPropertyAttributes(&it);
}
Maybe<bool> JSReceiver::HasElement(Handle<JSReceiver> object, uint32_t index) {
// Call the "has" trap on proxies.
if (object->IsJSProxy()) {
Isolate* isolate = object->GetIsolate();
Handle<Name> name = isolate->factory()->Uint32ToString(index);
Handle<JSProxy> proxy = Handle<JSProxy>::cast(object);
return JSProxy::HasPropertyWithHandler(proxy, name);
}
Maybe<PropertyAttributes> result = GetElementAttributes(object, index);
return result.IsJust() ? Just(result.FromJust() != ABSENT) : Nothing<bool>();
}
Maybe<bool> JSReceiver::HasOwnElement(Handle<JSReceiver> object,
uint32_t index) {
// Call the "has" trap on proxies.
if (object->IsJSProxy()) {
return JSProxy::GetElementAttributeWithHandler(
Handle<JSProxy>::cast(object), object, index);
Isolate* isolate = object->GetIsolate();
Handle<Name> name = isolate->factory()->Uint32ToString(index);
Handle<JSProxy> proxy = Handle<JSProxy>::cast(object);
return JSProxy::HasPropertyWithHandler(proxy, name);
}
return JSObject::GetElementAttributeWithReceiver(
Handle<JSObject>::cast(object), object, index, true);
Maybe<PropertyAttributes> result = GetOwnElementAttributes(object, index);
return result.IsJust() ? Just(result.FromJust() != ABSENT) : Nothing<bool>();
}
Maybe<PropertyAttributes> JSReceiver::GetElementAttributes(
Handle<JSReceiver> object, uint32_t index) {
Isolate* isolate = object->GetIsolate();
LookupIterator it(isolate, object, index);
return GetPropertyAttributes(&it);
}
Maybe<PropertyAttributes> JSReceiver::GetOwnElementAttributes(
Handle<JSReceiver> object, uint32_t index) {
Isolate* isolate = object->GetIsolate();
LookupIterator it(isolate, object, index, LookupIterator::HIDDEN);
return GetPropertyAttributes(&it);
}
......@@ -7032,40 +7081,6 @@ Object* JSReceiver::GetIdentityHash() {
}
Maybe<bool> JSReceiver::HasElement(Handle<JSReceiver> object, uint32_t index) {
if (object->IsJSProxy()) {
Handle<JSProxy> proxy = Handle<JSProxy>::cast(object);
return JSProxy::HasElementWithHandler(proxy, index);
}
Maybe<PropertyAttributes> result = JSObject::GetElementAttributeWithReceiver(
Handle<JSObject>::cast(object), object, index, true);
return result.IsJust() ? Just(result.FromJust() != ABSENT) : Nothing<bool>();
}
Maybe<bool> JSReceiver::HasOwnElement(Handle<JSReceiver> object,
uint32_t index) {
if (object->IsJSProxy()) {
Handle<JSProxy> proxy = Handle<JSProxy>::cast(object);
return JSProxy::HasElementWithHandler(proxy, index);
}
Maybe<PropertyAttributes> result = JSObject::GetElementAttributeWithReceiver(
Handle<JSObject>::cast(object), object, index, false);
return result.IsJust() ? Just(result.FromJust() != ABSENT) : Nothing<bool>();
}
Maybe<PropertyAttributes> JSReceiver::GetOwnElementAttribute(
Handle<JSReceiver> object, uint32_t index) {
if (object->IsJSProxy()) {
return JSProxy::GetElementAttributeWithHandler(
Handle<JSProxy>::cast(object), object, index);
}
return JSObject::GetElementAttributeWithReceiver(
Handle<JSObject>::cast(object), object, index, false);
}
bool AccessorInfo::all_can_read() {
return BooleanBit::get(flag(), kAllCanReadBit);
}
......
This diff is collapsed.
......@@ -1155,7 +1155,7 @@ class Object {
// Implementation of [[Put]], ECMA-262 5th edition, section 8.12.5.
MUST_USE_RESULT static MaybeHandle<Object> SetProperty(
Handle<Object> object, Handle<Name> key, Handle<Object> value,
Handle<Object> object, Handle<Name> name, Handle<Object> value,
LanguageMode language_mode,
StoreFromKeyed store_mode = MAY_BE_STORE_FROM_KEYED);
......@@ -1184,15 +1184,13 @@ class Object {
LookupIterator* it, Handle<Object> value, PropertyAttributes attributes,
LanguageMode language_mode, StoreFromKeyed store_mode);
MUST_USE_RESULT static inline MaybeHandle<Object> GetPropertyOrElement(
Handle<Object> object,
Handle<Name> key);
Handle<Object> object, Handle<Name> name);
MUST_USE_RESULT static inline MaybeHandle<Object> GetProperty(
Isolate* isolate,
Handle<Object> object,
const char* key);
MUST_USE_RESULT static inline MaybeHandle<Object> GetProperty(
Handle<Object> object,
Handle<Name> key);
Handle<Object> object, Handle<Name> name);
MUST_USE_RESULT static MaybeHandle<Object> GetPropertyWithAccessor(
LookupIterator* it);
......@@ -1676,18 +1674,20 @@ class JSReceiver: public HeapObject {
MUST_USE_RESULT static inline Maybe<PropertyAttributes> GetPropertyAttributes(
Handle<JSReceiver> object, Handle<Name> name);
MUST_USE_RESULT static Maybe<PropertyAttributes> GetPropertyAttributes(
LookupIterator* it);
MUST_USE_RESULT static Maybe<PropertyAttributes> GetOwnPropertyAttributes(
Handle<JSReceiver> object, Handle<Name> name);
MUST_USE_RESULT static inline Maybe<PropertyAttributes>
GetOwnPropertyAttributes(Handle<JSReceiver> object, Handle<Name> name);
MUST_USE_RESULT static inline Maybe<PropertyAttributes> GetElementAttribute(
MUST_USE_RESULT static inline Maybe<PropertyAttributes> GetElementAttributes(
Handle<JSReceiver> object, uint32_t index);
MUST_USE_RESULT static inline Maybe<PropertyAttributes>
GetOwnElementAttribute(Handle<JSReceiver> object, uint32_t index);
GetOwnElementAttributes(Handle<JSReceiver> object, uint32_t index);
MUST_USE_RESULT static Maybe<PropertyAttributes> GetPropertyAttributes(
LookupIterator* it);
static Handle<Object> GetDataProperty(Handle<JSReceiver> object,
Handle<Name> key);
Handle<Name> name);
static Handle<Object> GetDataProperty(LookupIterator* it);
......@@ -1831,13 +1831,11 @@ class JSObject: public JSReceiver {
};
MUST_USE_RESULT static MaybeHandle<Object> SetOwnPropertyIgnoreAttributes(
Handle<JSObject> object,
Handle<Name> key,
Handle<Object> value,
Handle<JSObject> object, Handle<Name> name, Handle<Object> value,
PropertyAttributes attributes,
ExecutableAccessorInfoHandling handling = DEFAULT_HANDLING);
static void AddProperty(Handle<JSObject> object, Handle<Name> key,
static void AddProperty(Handle<JSObject> object, Handle<Name> name,
Handle<Object> value, PropertyAttributes attributes);
// Extend the receiver with a single fast property appeared first in the
......@@ -1854,8 +1852,7 @@ class JSObject: public JSReceiver {
// Sets the property value in a normalized object given (key, value, details).
// Handles the special representation of JS global objects.
static void SetNormalizedProperty(Handle<JSObject> object,
Handle<Name> key,
static void SetNormalizedProperty(Handle<JSObject> object, Handle<Name> name,
Handle<Object> value,
PropertyDetails details);
......@@ -1879,10 +1876,6 @@ class JSObject: public JSReceiver {
GetPropertyAttributesWithInterceptor(LookupIterator* it);
MUST_USE_RESULT static Maybe<PropertyAttributes>
GetPropertyAttributesWithFailedAccessCheck(LookupIterator* it);
MUST_USE_RESULT static Maybe<PropertyAttributes>
GetElementAttributeWithReceiver(Handle<JSObject> object,
Handle<JSReceiver> receiver,
uint32_t index, bool check_prototype);
// Retrieves an AccessorPair property from the given object. Might return
// undefined if the property doesn't exist or is of a different kind.
......@@ -2041,11 +2034,11 @@ class JSObject: public JSReceiver {
// Support functions for v8 api (needed for correct interceptor behavior).
MUST_USE_RESULT static Maybe<bool> HasRealNamedProperty(
Handle<JSObject> object, Handle<Name> key);
Handle<JSObject> object, Handle<Name> name);
MUST_USE_RESULT static Maybe<bool> HasRealElementProperty(
Handle<JSObject> object, uint32_t index);
MUST_USE_RESULT static Maybe<bool> HasRealNamedCallbackProperty(
Handle<JSObject> object, Handle<Name> key);
Handle<JSObject> object, Handle<Name> name);
// Get the header size for a JSObject. Used to compute the index of
// internal fields as well as the number of internal fields.
......@@ -2299,31 +2292,6 @@ class JSObject: public JSReceiver {
MUST_USE_RESULT static MaybeHandle<Object> GetPropertyWithFailedAccessCheck(
LookupIterator* it);
MUST_USE_RESULT static Maybe<PropertyAttributes>
GetElementAttributeWithInterceptor(Handle<JSObject> object,
Handle<JSReceiver> receiver,
uint32_t index, bool continue_search);
// Queries indexed interceptor on an object for property attributes.
//
// We determine property attributes as follows:
// - if interceptor has a query callback, then the property attributes are
// the result of query callback for index.
// - otherwise if interceptor has a getter callback and it returns
// non-empty value on index, then the property attributes is NONE
// (property is present, and it is enumerable, configurable, writable)
// - otherwise there are no property attributes that can be inferred for
// interceptor, and this function returns ABSENT.
MUST_USE_RESULT static Maybe<PropertyAttributes>
GetElementAttributeFromInterceptor(Handle<JSObject> object,
Handle<Object> receiver,
uint32_t index);
MUST_USE_RESULT static Maybe<PropertyAttributes>
GetElementAttributeWithoutInterceptor(Handle<JSObject> object,
Handle<JSReceiver> receiver,
uint32_t index,
bool continue_search);
MUST_USE_RESULT static MaybeHandle<Object> SetElementWithCallback(
Handle<Object> object, Handle<Object> structure, uint32_t index,
Handle<Object> value, Handle<JSObject> holder,
......@@ -2347,11 +2315,6 @@ class JSObject: public JSReceiver {
MUST_USE_RESULT static MaybeHandle<Object> SetFastDoubleElement(
Handle<JSObject> object, uint32_t index, Handle<Object> value,
LanguageMode language_mode, bool check_prototype = true);
MUST_USE_RESULT static Maybe<PropertyAttributes>
GetElementAttributesWithFailedAccessCheck(Isolate* isolate,
Handle<JSObject> object,
Handle<Object> receiver,
uint32_t index);
MUST_USE_RESULT static MaybeHandle<Object> SetPropertyWithFailedAccessCheck(
LookupIterator* it, Handle<Object> value, LanguageMode language_mode);
......@@ -10017,10 +9980,6 @@ class JSProxy: public JSReceiver {
GetPropertyAttributesWithHandler(Handle<JSProxy> proxy,
Handle<Object> receiver,
Handle<Name> name);
MUST_USE_RESULT static Maybe<PropertyAttributes>
GetElementAttributeWithHandler(Handle<JSProxy> proxy,
Handle<JSReceiver> receiver,
uint32_t index);
MUST_USE_RESULT static MaybeHandle<Object> SetPropertyWithHandler(
Handle<JSProxy> proxy, Handle<Object> receiver, Handle<Name> name,
Handle<Object> value, LanguageMode language_mode);
......
......@@ -35,22 +35,7 @@ MaybeHandle<Object> Runtime::GetElementOrCharAt(Isolate* isolate,
if (!result->IsUndefined()) return result;
}
// Handle [] indexing on String objects
if (object->IsStringObjectWithCharacterAt(index)) {
Handle<JSValue> js_value = Handle<JSValue>::cast(object);
Handle<Object> result =
GetCharAt(Handle<String>(String::cast(js_value->value())), index);
if (!result->IsUndefined()) return result;
}
Handle<Object> result;
if (object->IsString() || object->IsNumber() || object->IsBoolean()) {
PrototypeIterator iter(isolate, object);
return Object::GetElement(isolate, PrototypeIterator::GetCurrent(iter),
index);
} else {
return Object::GetElement(isolate, object, index);
}
return Object::GetElement(isolate, object, index);
}
......@@ -360,61 +345,38 @@ MUST_USE_RESULT static MaybeHandle<Object> GetOwnProperty(Isolate* isolate,
Factory* factory = isolate->factory();
PropertyAttributes attrs;
uint32_t index = 0;
Handle<Object> value;
MaybeHandle<AccessorPair> maybe_accessors;
// TODO(verwaest): Unify once indexed properties can be handled by the
// LookupIterator.
if (name->AsArrayIndex(&index)) {
// Get attributes.
Maybe<PropertyAttributes> maybe =
JSReceiver::GetOwnElementAttribute(obj, index);
if (!maybe.IsJust()) return MaybeHandle<Object>();
attrs = maybe.FromJust();
if (attrs == ABSENT) return factory->undefined_value();
// Get AccessorPair if present.
maybe_accessors = JSObject::GetOwnElementAccessorPair(obj, index);
// Get value if not an AccessorPair.
if (maybe_accessors.is_null()) {
ASSIGN_RETURN_ON_EXCEPTION(
isolate, value, Runtime::GetElementOrCharAt(isolate, obj, index),
Object);
}
} else {
// Get attributes.
LookupIterator it(obj, name, LookupIterator::HIDDEN);
Maybe<PropertyAttributes> maybe = JSObject::GetPropertyAttributes(&it);
if (!maybe.IsJust()) return MaybeHandle<Object>();
attrs = maybe.FromJust();
if (attrs == ABSENT) return factory->undefined_value();
// Get AccessorPair if present.
if (it.state() == LookupIterator::ACCESSOR &&
it.GetAccessors()->IsAccessorPair()) {
maybe_accessors = Handle<AccessorPair>::cast(it.GetAccessors());
}
uint32_t index;
// Get attributes.
LookupIterator it =
name->AsArrayIndex(&index)
? LookupIterator(isolate, obj, index, LookupIterator::HIDDEN)
: LookupIterator(obj, name, LookupIterator::HIDDEN);
Maybe<PropertyAttributes> maybe = JSObject::GetPropertyAttributes(&it);
if (!maybe.IsJust()) return MaybeHandle<Object>();
attrs = maybe.FromJust();
if (attrs == ABSENT) return factory->undefined_value();
// Get value if not an AccessorPair.
if (maybe_accessors.is_null()) {
ASSIGN_RETURN_ON_EXCEPTION(isolate, value, Object::GetProperty(&it),
Object);
}
}
DCHECK(!isolate->has_pending_exception());
Handle<FixedArray> elms = factory->NewFixedArray(DESCRIPTOR_SIZE);
elms->set(ENUMERABLE_INDEX, heap->ToBoolean((attrs & DONT_ENUM) == 0));
elms->set(CONFIGURABLE_INDEX, heap->ToBoolean((attrs & DONT_DELETE) == 0));
elms->set(IS_ACCESSOR_INDEX, heap->ToBoolean(!maybe_accessors.is_null()));
Handle<AccessorPair> accessors;
if (maybe_accessors.ToHandle(&accessors)) {
bool is_accessor_pair = it.state() == LookupIterator::ACCESSOR &&
it.GetAccessors()->IsAccessorPair();
elms->set(IS_ACCESSOR_INDEX, heap->ToBoolean(is_accessor_pair));
if (is_accessor_pair) {
Handle<AccessorPair> accessors =
Handle<AccessorPair>::cast(it.GetAccessors());
Handle<Object> getter(accessors->GetComponent(ACCESSOR_GETTER), isolate);
Handle<Object> setter(accessors->GetComponent(ACCESSOR_SETTER), isolate);
elms->set(GETTER_INDEX, *getter);
elms->set(SETTER_INDEX, *setter);
} else {
Handle<Object> value;
ASSIGN_RETURN_ON_EXCEPTION(isolate, value, Object::GetProperty(&it),
Object);
elms->set(WRITABLE_INDEX, heap->ToBoolean((attrs & READ_ONLY) == 0));
elms->set(VALUE_INDEX, *value);
}
......
......@@ -3103,21 +3103,25 @@ THREADED_TEST(IndexedAllCanReadInterceptor) {
ExpectInt32("checked[15]", 17);
CHECK(!CompileRun("Object.getOwnPropertyDescriptor(checked, '15')")
->IsUndefined());
CHECK_EQ(3, access_check_data.count);
CHECK_EQ(2, access_check_data.count);
access_check_data.result = false;
ExpectInt32("checked[15]", intercept_data_0.value);
// Note: this should throw but without a LookupIterator it's complicated.
CHECK(!CompileRun("Object.getOwnPropertyDescriptor(checked, '15')")
->IsUndefined());
CHECK_EQ(6, access_check_data.count);
{
v8::TryCatch try_catch(isolate);
CompileRun("Object.getOwnPropertyDescriptor(checked, '15')");
CHECK(try_catch.HasCaught());
}
CHECK_EQ(4, access_check_data.count);
intercept_data_1.should_intercept = true;
ExpectInt32("checked[15]", intercept_data_1.value);
// Note: this should throw but without a LookupIterator it's complicated.
CHECK(!CompileRun("Object.getOwnPropertyDescriptor(checked, '15')")
->IsUndefined());
CHECK_EQ(9, access_check_data.count);
{
v8::TryCatch try_catch(isolate);
CompileRun("Object.getOwnPropertyDescriptor(checked, '15')");
CHECK(try_catch.HasCaught());
}
CHECK_EQ(6, access_check_data.count);
}
......
......@@ -56,9 +56,9 @@ PASS checkNumericGet(true, Boolean) is true
FAIL checkNumericSet(1, Number) should be true. Was false.
FAIL checkNumericSet('hello', String) should be true. Was false.
FAIL checkNumericSet(true, Boolean) should be true. Was false.
FAIL checkNumericGetStrict(1, Number) should be true. Was false.
FAIL checkNumericGetStrict('hello', String) should be true. Was false.
FAIL checkNumericGetStrict(true, Boolean) should be true. Was false.
PASS checkNumericGetStrict(1, Number) is true
PASS checkNumericGetStrict('hello', String) is true
PASS checkNumericGetStrict(true, Boolean) is true
FAIL checkNumericSetStrict(1, Number) should be true. Was false.
FAIL checkNumericSetStrict('hello', String) should be true. Was false.
FAIL checkNumericSetStrict(true, Boolean) should be true. Was false.
......
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