Commit dec3de8a authored by Igor Sheludko's avatar Igor Sheludko Committed by Commit Bot

[builtins] Make ToIndex() uintptr index friendly

The new ToIndex() must eventually replace ToSmiIndex().

The CL fixes the following abstract operations:
  GetViewValue(view, requestIndex, isLittleEndian, type)
  SetViewValue(view, requestIndex, isLittleEndian, type, value)

and the following builtins:
  DataView.prototype.getXXX
  DataView.prototype.setXXX

where XXX are all typed elements.

Bug: v8:4153
Change-Id: Ic2f33e91b59426deb0efa28bb4c15253e80a299c
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/1874345
Commit-Queue: Igor Sheludko <ishell@chromium.org>
Reviewed-by: 's avatarJakob Kummerow <jkummerow@chromium.org>
Cr-Commit-Position: refs/heads/master@{#64506}
parent 1e256fc3
...@@ -1407,6 +1407,7 @@ const kArrayBufferMaxByteLength: ...@@ -1407,6 +1407,7 @@ const kArrayBufferMaxByteLength:
const kMaxTypedArrayInHeap: const kMaxTypedArrayInHeap:
constexpr int31 generates 'JSTypedArray::kMaxSizeInHeap'; constexpr int31 generates 'JSTypedArray::kMaxSizeInHeap';
const kMaxSafeInteger: constexpr float64 generates 'kMaxSafeInteger'; const kMaxSafeInteger: constexpr float64 generates 'kMaxSafeInteger';
const kMaxUInt32Double: constexpr float64 generates 'kMaxUInt32Double';
const kSmiMaxValue: constexpr uintptr generates 'kSmiMaxValue'; const kSmiMaxValue: constexpr uintptr generates 'kSmiMaxValue';
const kSmiMax: uintptr = kSmiMaxValue; const kSmiMax: uintptr = kSmiMaxValue;
// TODO(v8:8996): Use uintptr version instead and drop this one. // TODO(v8:8996): Use uintptr version instead and drop this one.
...@@ -1854,6 +1855,7 @@ extern transitioning macro ToInteger_Inline( ...@@ -1854,6 +1855,7 @@ extern transitioning macro ToInteger_Inline(
Context, JSAny, constexpr ToIntegerTruncationMode): Number; Context, JSAny, constexpr ToIntegerTruncationMode): Number;
extern transitioning macro ToLength_Inline(Context, JSAny): Number; extern transitioning macro ToLength_Inline(Context, JSAny): Number;
extern transitioning macro ToNumber_Inline(Context, JSAny): Number; extern transitioning macro ToNumber_Inline(Context, JSAny): Number;
// TODO(v8:4153): Use ToIndex() instead.
extern transitioning macro ToSmiIndex(implicit context: Context)(JSAny): extern transitioning macro ToSmiIndex(implicit context: Context)(JSAny):
PositiveSmi labels IfRangeError; PositiveSmi labels IfRangeError;
extern transitioning macro ToSmiLength(implicit context: Context)(JSAny): extern transitioning macro ToSmiLength(implicit context: Context)(JSAny):
...@@ -3615,18 +3617,46 @@ macro SameValue(a: JSAny, b: JSAny): bool { ...@@ -3615,18 +3617,46 @@ macro SameValue(a: JSAny, b: JSAny): bool {
BranchIfSameValue(a, b) otherwise return true, return false; BranchIfSameValue(a, b) otherwise return true, return false;
} }
transitioning macro ToIndex(input: JSAny, context: Context): Number // https://tc39.github.io/ecma262/#sec-toindex
labels RangeError { @export
if (input == Undefined) { transitioning macro ToIndex(implicit context: Context)(value: JSAny):
return 0; uintptr labels IfRangeError {
} if (value == Undefined) return 0;
const indexNumber = ToInteger_Inline(context, value, kTruncateMinusZero);
return ToIndex(indexNumber) otherwise IfRangeError;
}
const value: Number = ToInteger_Inline(context, input, kTruncateMinusZero); // https://tc39.github.io/ecma262/#sec-toindex
if (value < 0 || value > kMaxSafeInteger) { // Same as the version above but for Number arguments.
goto RangeError; macro ToIndex(indexNumber: Number): uintptr labels IfRangeError {
} typeswitch (indexNumber) {
case (indexSmi: Smi): {
if (indexSmi < 0) goto IfRangeError;
const index: uintptr = Unsigned(Convert<intptr>(indexSmi));
// Positive Smi values definitely fit into both [0, kMaxSafeInteger] and
// [0, kMaxUintPtr] ranges.
return index;
}
case (indexHeapNumber: HeapNumber): {
assert(IsNumberNormalized(indexHeapNumber));
const indexDouble: float64 = Convert<float64>(indexHeapNumber);
// NaNs must already be handled by ToIndex() version above accepting
// JSAny indices.
assert(!Float64IsNaN(indexDouble));
if (indexDouble < 0) goto IfRangeError;
return value; if constexpr (Is64()) {
if (indexDouble > kMaxSafeInteger) goto IfRangeError;
} else {
// On 32-bit architectures not all safe integers fit into uintptr but
// callers handle this case the same way as the safe integer range
// overflow case so we don't need special handling for the values in
// (kMaxUInt32, kMaxSafeInteger] range.
if (indexDouble > kMaxUInt32Double) goto IfRangeError;
}
return ChangeFloat64ToUintPtr(indexDouble);
}
}
} }
transitioning macro GetLengthProperty(implicit context: Context)(o: JSAny): transitioning macro GetLengthProperty(implicit context: Context)(o: JSAny):
...@@ -3694,7 +3724,7 @@ macro ConvertToRelativeIndex(indexNumber: Number, length: uintptr): uintptr { ...@@ -3694,7 +3724,7 @@ macro ConvertToRelativeIndex(indexNumber: Number, length: uintptr): uintptr {
const indexDouble: float64 = Convert<float64>(indexHeapNumber); const indexDouble: float64 = Convert<float64>(indexHeapNumber);
// NaNs must already be handled by ConvertToRelativeIndex() version // NaNs must already be handled by ConvertToRelativeIndex() version
// above accepting JSAny indices. // above accepting JSAny indices.
assert(indexDouble == indexDouble); assert(!Float64IsNaN(indexDouble));
const lengthDouble: float64 = Convert<float64>(length); const lengthDouble: float64 = Convert<float64>(length);
assert(lengthDouble <= kMaxSafeInteger); assert(lengthDouble <= kMaxSafeInteger);
if (indexDouble < 0) { if (indexDouble < 0) {
......
...@@ -351,67 +351,90 @@ namespace data_view { ...@@ -351,67 +351,90 @@ namespace data_view {
return MakeBigInt(lowWord, highWord, signed); return MakeBigInt(lowWord, highWord, signed);
} }
extern macro ToSmiIndex(JSAny, Context): Smi
labels RangeError;
extern macro DataViewBuiltinsAssembler::DataViewElementSize( extern macro DataViewBuiltinsAssembler::DataViewElementSize(
constexpr ElementsKind): constexpr int31; constexpr ElementsKind): constexpr int31;
// GetViewValue ( view, requestIndex, isLittleEndian, type )
// https://tc39.es/ecma262/#sec-getviewvalue
transitioning macro DataViewGet( transitioning macro DataViewGet(
context: Context, receiver: JSAny, offset: JSAny, context: Context, receiver: JSAny, requestIndex: JSAny,
requestedLittleEndian: JSAny, kind: constexpr ElementsKind): Numeric { requestedLittleEndian: JSAny, kind: constexpr ElementsKind): Numeric {
// 1. Perform ? RequireInternalSlot(view, [[DataView]]).
// 2. Assert: view has a [[ViewedArrayBuffer]] internal slot.
const dataView: JSDataView = const dataView: JSDataView =
ValidateDataView(context, receiver, MakeDataViewGetterNameString(kind)); ValidateDataView(context, receiver, MakeDataViewGetterNameString(kind));
let getIndex: Number;
try { try {
getIndex = ToIndex(offset, context) otherwise RangeError; // 3. Let getIndex be ? ToIndex(requestIndex).
} const getIndex: uintptr = ToIndex(requestIndex) otherwise RangeError;
label RangeError {
ThrowRangeError(kInvalidDataViewAccessorOffset);
}
const littleEndian: bool = ToBoolean(requestedLittleEndian); // 4. Set isLittleEndian to ! ToBoolean(isLittleEndian).
const buffer: JSArrayBuffer = dataView.buffer; const littleEndian: bool = ToBoolean(requestedLittleEndian);
if (IsDetachedBuffer(buffer)) { // 5. Let buffer be view.[[ViewedArrayBuffer]].
ThrowTypeError(kDetachedOperation, MakeDataViewGetterNameString(kind)); const buffer: JSArrayBuffer = dataView.buffer;
}
const getIndexFloat: float64 = Convert<float64>(getIndex); // 6. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
const getIndexWord: uintptr = Convert<uintptr>(getIndexFloat); if (IsDetachedBuffer(buffer)) {
ThrowTypeError(kDetachedOperation, MakeDataViewGetterNameString(kind));
}
const viewOffsetWord: uintptr = dataView.byte_offset; // 7. Let viewOffset be view.[[ByteOffset]].
const viewSizeFloat: float64 = Convert<float64>(dataView.byte_length); const viewOffset: uintptr = dataView.byte_offset;
const elementSizeFloat: float64 = DataViewElementSize(kind);
if (getIndexFloat + elementSizeFloat > viewSizeFloat) { // 8. Let viewSize be view.[[ByteLength]].
ThrowRangeError(kInvalidDataViewAccessorOffset); const viewSize: uintptr = dataView.byte_length;
}
const bufferIndex: uintptr = getIndexWord + viewOffsetWord; // 9. Let elementSize be the Element Size value specified in Table 62
// for Element Type type.
const elementSize: uintptr = DataViewElementSize(kind);
if constexpr (kind == UINT8_ELEMENTS) { // 10. If getIndex + elementSize > viewSize, throw a RangeError exception.
return LoadDataView8(buffer, bufferIndex, false); if constexpr (Is64()) {
} else if constexpr (kind == INT8_ELEMENTS) { // Given that
return LoadDataView8(buffer, bufferIndex, true); // a) getIndex is in [0, kMaxSafeInteger] range
} else if constexpr (kind == UINT16_ELEMENTS) { // b) elementSize is in [1, 8] range
return LoadDataView16(buffer, bufferIndex, littleEndian, false); // the addition can't overflow.
} else if constexpr (kind == INT16_ELEMENTS) { if (getIndex + elementSize > viewSize) goto RangeError;
return LoadDataView16(buffer, bufferIndex, littleEndian, true); } else {
} else if constexpr (kind == UINT32_ELEMENTS) { // In order to avoid operating on float64s we deal with uintptr values
return LoadDataView32(buffer, bufferIndex, littleEndian, kind); // and do two comparisons to handle potential uintptr overflow on
} else if constexpr (kind == INT32_ELEMENTS) { // 32-bit architectures.
return LoadDataView32(buffer, bufferIndex, littleEndian, kind); const lastPossibleElementOffset: uintptr = viewSize - elementSize;
} else if constexpr (kind == FLOAT32_ELEMENTS) { // Check if lastPossibleElementOffset underflowed.
return LoadDataView32(buffer, bufferIndex, littleEndian, kind); if (lastPossibleElementOffset > viewSize) goto RangeError;
} else if constexpr (kind == FLOAT64_ELEMENTS) { if (getIndex > lastPossibleElementOffset) goto RangeError;
return LoadDataViewFloat64(buffer, bufferIndex, littleEndian); }
} else if constexpr (kind == BIGUINT64_ELEMENTS) {
return LoadDataViewBigInt(buffer, bufferIndex, littleEndian, false); // 11. Let bufferIndex be getIndex + viewOffset.
} else if constexpr (kind == BIGINT64_ELEMENTS) { const bufferIndex: uintptr = getIndex + viewOffset;
return LoadDataViewBigInt(buffer, bufferIndex, littleEndian, true);
} else { if constexpr (kind == UINT8_ELEMENTS) {
unreachable; return LoadDataView8(buffer, bufferIndex, false);
} else if constexpr (kind == INT8_ELEMENTS) {
return LoadDataView8(buffer, bufferIndex, true);
} else if constexpr (kind == UINT16_ELEMENTS) {
return LoadDataView16(buffer, bufferIndex, littleEndian, false);
} else if constexpr (kind == INT16_ELEMENTS) {
return LoadDataView16(buffer, bufferIndex, littleEndian, true);
} else if constexpr (kind == UINT32_ELEMENTS) {
return LoadDataView32(buffer, bufferIndex, littleEndian, kind);
} else if constexpr (kind == INT32_ELEMENTS) {
return LoadDataView32(buffer, bufferIndex, littleEndian, kind);
} else if constexpr (kind == FLOAT32_ELEMENTS) {
return LoadDataView32(buffer, bufferIndex, littleEndian, kind);
} else if constexpr (kind == FLOAT64_ELEMENTS) {
return LoadDataViewFloat64(buffer, bufferIndex, littleEndian);
} else if constexpr (kind == BIGUINT64_ELEMENTS) {
return LoadDataViewBigInt(buffer, bufferIndex, littleEndian, false);
} else if constexpr (kind == BIGINT64_ELEMENTS) {
return LoadDataViewBigInt(buffer, bufferIndex, littleEndian, true);
} else {
unreachable;
}
}
label RangeError {
ThrowRangeError(kInvalidDataViewAccessorOffset);
} }
} }
...@@ -631,90 +654,104 @@ namespace data_view { ...@@ -631,90 +654,104 @@ namespace data_view {
StoreDataView64(buffer, offset, lowWord, highWord, requestedLittleEndian); StoreDataView64(buffer, offset, lowWord, highWord, requestedLittleEndian);
} }
// SetViewValue ( view, requestIndex, isLittleEndian, type, value )
// https://tc39.es/ecma262/#sec-setviewvalue
transitioning macro DataViewSet( transitioning macro DataViewSet(
context: Context, receiver: JSAny, offset: JSAny, value: JSAny, context: Context, receiver: JSAny, requestIndex: JSAny, value: JSAny,
requestedLittleEndian: JSAny, kind: constexpr ElementsKind): JSAny { requestedLittleEndian: JSAny, kind: constexpr ElementsKind): JSAny {
// 1. Perform ? RequireInternalSlot(view, [[DataView]]).
// 2. Assert: view has a [[ViewedArrayBuffer]] internal slot.
const dataView: JSDataView = const dataView: JSDataView =
ValidateDataView(context, receiver, MakeDataViewSetterNameString(kind)); ValidateDataView(context, receiver, MakeDataViewSetterNameString(kind));
let getIndex: Number;
try { try {
getIndex = ToIndex(offset, context) otherwise RangeError; // 3. Let getIndex be ? ToIndex(requestIndex).
} const getIndex: uintptr = ToIndex(requestIndex) otherwise RangeError;
label RangeError {
ThrowRangeError(kInvalidDataViewAccessorOffset);
}
const littleEndian: bool = ToBoolean(requestedLittleEndian); const littleEndian: bool = ToBoolean(requestedLittleEndian);
const buffer: JSArrayBuffer = dataView.buffer; const buffer: JSArrayBuffer = dataView.buffer;
// According to ES6 section 24.2.1.2 SetViewValue, we must perform let numberValue: Numeric;
// the conversion before doing the bounds check. if constexpr (kind == BIGUINT64_ELEMENTS || kind == BIGINT64_ELEMENTS) {
if constexpr (kind == BIGUINT64_ELEMENTS || kind == BIGINT64_ELEMENTS) { // 4. If ! IsBigIntElementType(type) is true, let numberValue be
const bigIntValue: BigInt = ToBigInt(context, value); // ? ToBigInt(value).
numberValue = ToBigInt(context, value);
} else {
// 5. Otherwise, let numberValue be ? ToNumber(value).
numberValue = ToNumber(context, value);
}
// 6. If IsDetachedBuffer(buffer) is true, throw a TypeError exception.
if (IsDetachedBuffer(buffer)) { if (IsDetachedBuffer(buffer)) {
ThrowTypeError(kDetachedOperation, MakeDataViewSetterNameString(kind)); ThrowTypeError(kDetachedOperation, MakeDataViewSetterNameString(kind));
} }
const getIndexFloat: float64 = Convert<float64>(getIndex); // 9. Let viewOffset be view.[[ByteOffset]].
const getIndexWord: uintptr = Convert<uintptr>(getIndexFloat); const viewOffset: uintptr = dataView.byte_offset;
const viewOffsetWord: uintptr = dataView.byte_offset; // 10. Let viewSize be view.[[ByteLength]].
const viewSizeFloat: float64 = Convert<float64>(dataView.byte_length); const viewSize: uintptr = dataView.byte_length;
const elementSizeFloat: float64 = DataViewElementSize(kind);
if (getIndexFloat + elementSizeFloat > viewSizeFloat) { // 11. Let elementSize be the Element Size value specified in Table 62
ThrowRangeError(kInvalidDataViewAccessorOffset); // for Element Type type.
} const elementSize: uintptr = DataViewElementSize(kind);
const bufferIndex: uintptr = getIndexWord + viewOffsetWord; // 12. If getIndex + elementSize > viewSize, throw a RangeError exception.
StoreDataViewBigInt(buffer, bufferIndex, bigIntValue, littleEndian); if constexpr (Is64()) {
} else { // Given that
const numValue: Number = ToNumber(context, value); // a) getIndex is in [0, kMaxSafeInteger] range
// b) elementSize is in [1, 8] range
if (IsDetachedBuffer(buffer)) { // the addition can't overflow.
ThrowTypeError(kDetachedOperation, MakeDataViewSetterNameString(kind)); if (getIndex + elementSize > viewSize) goto RangeError;
} else {
// In order to avoid operating on float64s we deal with uintptr values
// and do two comparisons to handle potential uintptr overflow on
// 32-bit architectures.
const lastPossibleElementOffset: uintptr = viewSize - elementSize;
// Check if lastPossibleElementOffset underflowed.
if (lastPossibleElementOffset > viewSize) goto RangeError;
if (getIndex > lastPossibleElementOffset) goto RangeError;
} }
const getIndexFloat: float64 = Convert<float64>(getIndex); // 13. Let bufferIndex be getIndex + viewOffset.
const getIndexWord: uintptr = Convert<uintptr>(getIndexFloat); const bufferIndex: uintptr = getIndex + viewOffset;
const viewOffsetWord: uintptr = dataView.byte_offset;
const viewSizeFloat: float64 = Convert<float64>(dataView.byte_length);
const elementSizeFloat: float64 = DataViewElementSize(kind);
if (getIndexFloat + elementSizeFloat > viewSizeFloat) { if constexpr (kind == BIGUINT64_ELEMENTS || kind == BIGINT64_ELEMENTS) {
ThrowRangeError(kInvalidDataViewAccessorOffset); // For these elements kinds numberValue is BigInt.
} const bigIntValue: BigInt = %RawDownCast<BigInt>(numberValue);
StoreDataViewBigInt(buffer, bufferIndex, bigIntValue, littleEndian);
const bufferIndex: uintptr = getIndexWord + viewOffsetWord; } else {
// For these elements kinds numberValue is Number.
const doubleValue: float64 = ChangeNumberToFloat64(numValue); const numValue: Number = %RawDownCast<Number>(numberValue);
const doubleValue: float64 = ChangeNumberToFloat64(numValue);
if constexpr (kind == UINT8_ELEMENTS || kind == INT8_ELEMENTS) {
StoreDataView8( if constexpr (kind == UINT8_ELEMENTS || kind == INT8_ELEMENTS) {
buffer, bufferIndex, TruncateFloat64ToWord32(doubleValue)); StoreDataView8(
} else if constexpr (kind == UINT16_ELEMENTS || kind == INT16_ELEMENTS) { buffer, bufferIndex, TruncateFloat64ToWord32(doubleValue));
StoreDataView16( } else if constexpr (kind == UINT16_ELEMENTS || kind == INT16_ELEMENTS) {
buffer, bufferIndex, TruncateFloat64ToWord32(doubleValue), StoreDataView16(
littleEndian); buffer, bufferIndex, TruncateFloat64ToWord32(doubleValue),
} else if constexpr (kind == UINT32_ELEMENTS || kind == INT32_ELEMENTS) { littleEndian);
StoreDataView32( } else if constexpr (kind == UINT32_ELEMENTS || kind == INT32_ELEMENTS) {
buffer, bufferIndex, TruncateFloat64ToWord32(doubleValue), StoreDataView32(
littleEndian); buffer, bufferIndex, TruncateFloat64ToWord32(doubleValue),
} else if constexpr (kind == FLOAT32_ELEMENTS) { littleEndian);
const floatValue: float32 = TruncateFloat64ToFloat32(doubleValue); } else if constexpr (kind == FLOAT32_ELEMENTS) {
StoreDataView32( const floatValue: float32 = TruncateFloat64ToFloat32(doubleValue);
buffer, bufferIndex, BitcastFloat32ToInt32(floatValue), StoreDataView32(
littleEndian); buffer, bufferIndex, BitcastFloat32ToInt32(floatValue),
} else if constexpr (kind == FLOAT64_ELEMENTS) { littleEndian);
const lowWord: uint32 = Float64ExtractLowWord32(doubleValue); } else if constexpr (kind == FLOAT64_ELEMENTS) {
const highWord: uint32 = Float64ExtractHighWord32(doubleValue); const lowWord: uint32 = Float64ExtractLowWord32(doubleValue);
StoreDataView64(buffer, bufferIndex, lowWord, highWord, littleEndian); const highWord: uint32 = Float64ExtractHighWord32(doubleValue);
StoreDataView64(buffer, bufferIndex, lowWord, highWord, littleEndian);
}
} }
return Undefined;
}
label RangeError {
ThrowRangeError(kInvalidDataViewAccessorOffset);
} }
return Undefined;
} }
transitioning javascript builtin DataViewPrototypeSetUint8( transitioning javascript builtin DataViewPrototypeSetUint8(
......
...@@ -212,17 +212,12 @@ namespace typed_array { ...@@ -212,17 +212,12 @@ namespace typed_array {
map: Map, buffer: JSArrayBuffer, byteOffset: JSAny, length: JSAny, map: Map, buffer: JSArrayBuffer, byteOffset: JSAny, length: JSAny,
elementsInfo: typed_array::TypedArrayElementsInfo): JSTypedArray { elementsInfo: typed_array::TypedArrayElementsInfo): JSTypedArray {
try { try {
let offset: uintptr = 0; // 6. Let offset be ? ToIndex(byteOffset).
if (byteOffset != Undefined) { const offset: uintptr = ToIndex(byteOffset) otherwise IfInvalidOffset;
// 6. Let offset be ? ToIndex(byteOffset).
offset = TryNumberToUintPtr( // 7. If offset modulo elementSize ≠ 0, throw a RangeError exception.
ToInteger_Inline(context, byteOffset, kTruncateMinusZero)) if (elementsInfo.IsUnaligned(offset)) {
otherwise goto IfInvalidOffset; goto IfInvalidAlignment('start offset');
// 7. If offset modulo elementSize ≠ 0, throw a RangeError exception.
if (elementsInfo.IsUnaligned(offset)) {
goto IfInvalidAlignment('start offset');
}
} }
let newLength: PositiveSmi = 0; let newLength: PositiveSmi = 0;
......
...@@ -2664,6 +2664,7 @@ class V8_EXPORT_PRIVATE CodeStubAssembler ...@@ -2664,6 +2664,7 @@ class V8_EXPORT_PRIVATE CodeStubAssembler
}; };
// ES6 7.1.17 ToIndex, but jumps to range_error if the result is not a Smi. // ES6 7.1.17 ToIndex, but jumps to range_error if the result is not a Smi.
// TODO(v8:4153): Use ToIndex() instead.
TNode<Smi> ToSmiIndex(TNode<Context> context, TNode<Object> input, TNode<Smi> ToSmiIndex(TNode<Context> context, TNode<Object> input,
Label* range_error); Label* range_error);
......
...@@ -1043,6 +1043,8 @@ constexpr uint64_t kHoleNanInt64 = ...@@ -1043,6 +1043,8 @@ constexpr uint64_t kHoleNanInt64 =
// ES6 section 20.1.2.6 Number.MAX_SAFE_INTEGER // ES6 section 20.1.2.6 Number.MAX_SAFE_INTEGER
constexpr double kMaxSafeInteger = 9007199254740991.0; // 2^53-1 constexpr double kMaxSafeInteger = 9007199254740991.0; // 2^53-1
constexpr double kMaxUInt32Double = double{kMaxUInt32};
// The order of this enum has to be kept in sync with the predicates below. // The order of this enum has to be kept in sync with the predicates below.
enum class VariableMode : uint8_t { enum class VariableMode : uint8_t {
// User declared variables: // User declared variables:
......
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