Commit 069e6b2f authored by titzer's avatar titzer Committed by Commit bot

[turbofan] Optimize loads of global constants in JSTypedLowering.

R=mstarzinger@chromium.org,verwaest@chromium.org
BUG=

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

Cr-Commit-Position: refs/heads/master@{#27792}
parent 0e539d1c
......@@ -747,6 +747,27 @@ Reduction JSTypedLowering::ReduceJSToString(Node* node) {
}
static bool IsGlobalObject(Node* node) {
return NodeProperties::IsTyped(node) &&
NodeProperties::GetBounds(node).upper->Is(Type::GlobalObject());
}
Reduction JSTypedLowering::ReduceJSLoadNamed(Node* node) {
if (IsGlobalObject(node->InputAt(0))) {
// Optimize global constants like "undefined", "Infinity", and "NaN".
Handle<Name> name = LoadNamedParametersOf(node->op()).name().handle();
Handle<Object> constant_value = factory()->GlobalConstantFor(name);
if (!constant_value.is_null()) {
Node* constant = jsgraph()->Constant(constant_value);
NodeProperties::ReplaceWithValue(node, constant);
return Replace(constant);
}
}
return NoChange();
}
Reduction JSTypedLowering::ReduceJSLoadProperty(Node* node) {
Node* key = NodeProperties::GetValueInput(node, 1);
Node* base = NodeProperties::GetValueInput(node, 0);
......@@ -996,6 +1017,8 @@ Reduction JSTypedLowering::Reduce(Node* node) {
return ReduceJSToNumber(node);
case IrOpcode::kJSToString:
return ReduceJSToString(node);
case IrOpcode::kJSLoadNamed:
return ReduceJSLoadNamed(node);
case IrOpcode::kJSLoadProperty:
return ReduceJSLoadProperty(node);
case IrOpcode::kJSStoreProperty:
......
......@@ -35,6 +35,7 @@ class JSTypedLowering FINAL : public Reducer {
Reduction ReduceJSBitwiseOr(Node* node);
Reduction ReduceJSMultiply(Node* node);
Reduction ReduceJSComparison(Node* node);
Reduction ReduceJSLoadNamed(Node* node);
Reduction ReduceJSLoadProperty(Node* node);
Reduction ReduceJSStoreProperty(Node* node);
Reduction ReduceJSLoadContext(Node* node);
......
......@@ -1444,18 +1444,21 @@ Bounds Typer::Visitor::TypeJSInstanceOf(Node* node) {
Bounds Typer::Visitor::TypeJSLoadContext(Node* node) {
ContextAccess access = OpParameter<ContextAccess>(node);
Bounds outer = Operand(node, 0);
Type* context_type = outer.upper;
Type* upper = (access.index() == Context::GLOBAL_OBJECT_INDEX)
? Type::GlobalObject()
: Type::Any();
if (context_type->Is(Type::None())) {
// Upper bound of context is not yet known.
return Bounds(Type::None(), Type::Any());
return Bounds(Type::None(), upper);
}
DCHECK(context_type->Maybe(Type::Internal()));
// TODO(rossberg): More precisely, instead of the above assertion, we should
// back-propagate the constraint that it has to be a subtype of Internal.
ContextAccess access = OpParameter<ContextAccess>(node);
MaybeHandle<Context> context;
if (context_type->IsConstant()) {
context = Handle<Context>::cast(context_type->AsConstant()->Value());
......@@ -1463,8 +1466,6 @@ Bounds Typer::Visitor::TypeJSLoadContext(Node* node) {
// Walk context chain (as far as known), mirroring dynamic lookup.
// Since contexts are mutable, the information is only useful as a lower
// bound.
// TODO(rossberg): Could use scope info to fix upper bounds for constant
// bindings if we know that this code is never shared.
for (size_t i = access.depth(); i > 0; --i) {
if (context_type->IsContext()) {
context_type = context_type->AsContext()->Outer();
......@@ -1475,15 +1476,13 @@ Bounds Typer::Visitor::TypeJSLoadContext(Node* node) {
context = handle(context.ToHandleChecked()->previous(), isolate());
}
}
if (context.is_null()) {
return Bounds::Unbounded(zone());
} else {
Handle<Object> value =
Type* lower = Type::None();
if (!context.is_null()) {
lower = TypeConstant(
handle(context.ToHandleChecked()->get(static_cast<int>(access.index())),
isolate());
Type* lower = TypeConstant(value);
return Bounds(lower, Type::Any());
isolate()));
}
return Bounds(lower, upper);
}
......
......@@ -2342,10 +2342,10 @@ void Factory::SetRegExpIrregexpData(Handle<JSRegExp> regexp,
}
Handle<Object> Factory::GlobalConstantFor(Handle<String> name) {
if (String::Equals(name, undefined_string())) return undefined_value();
if (String::Equals(name, nan_string())) return nan_value();
if (String::Equals(name, infinity_string())) return infinity_value();
Handle<Object> Factory::GlobalConstantFor(Handle<Name> name) {
if (Name::Equals(name, undefined_string())) return undefined_value();
if (Name::Equals(name, nan_string())) return nan_value();
if (Name::Equals(name, infinity_string())) return infinity_value();
return Handle<Object>::null();
}
......
......@@ -668,7 +668,7 @@ class Factory FINAL {
// Returns the value for a known global constant (a property of the global
// object which is neither configurable nor writable) like 'undefined'.
// Returns a null handle when the given name is unknown.
Handle<Object> GlobalConstantFor(Handle<String> name);
Handle<Object> GlobalConstantFor(Handle<Name> name);
// Converts the given boolean condition to JavaScript boolean value.
Handle<Object> ToBoolean(bool value);
......
......@@ -277,6 +277,7 @@ TypeImpl<Config>::BitsetType::Lub(i::Map* map) {
case FOREIGN_TYPE:
case SCRIPT_TYPE:
case CODE_TYPE:
case PROPERTY_CELL_TYPE:
return kInternal & kTaggedPointer;
default:
UNREACHABLE();
......
// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
"use strict";
function test(expected, f) {
assertEquals(expected, f());
assertEquals(expected, f());
%OptimizeFunctionOnNextCall(f);
assertEquals(expected, f());
assertEquals(expected, f());
}
function testThrows(f) {
assertThrows(f);
assertThrows(f);
%OptimizeFunctionOnNextCall(f);
assertThrows(f);
assertThrows(f);
}
function f1() { return Infinity; }
test((1/0), f1);
function f2() { return (1/0); }
test((1/0), f2);
function f3() { return (1/0) == (1/0); }
test(true, f3);
function f4() { return (1/0) == Infinity; }
test(true, f4);
function f5() { return Infinity == (1/0); }
test(true, f5);
function f6() { return "" + Infinity; }
test("Infinity", f6);
function f7() { return (1/0) === (1/0); }
test(true, f7);
function f8() { return (1/0) === Infinity; }
test(true, f8);
function f9() { return Infinity === (1/0); }
test(true, f9);
// --
function g1() { return Infinity; }
test((1/0), g1);
function g2() { return (1/0); }
test((1/0), g2);
function g3() { return (1/0) == (1/0); }
test(true, g3);
function g4() { return (1/0) == Infinity; }
test(true, g4);
function g5() { return Infinity == (1/0); }
test(true, g5);
function g6() { return "" + Infinity; }
test("Infinity", g6);
function g7() { return (1/0) === (1/0); }
test(true, g7);
function g8() { return (1/0) === Infinity; }
test(true, g8);
function g9() { return Infinity === (1/0); }
test(true, g9);
testThrows(function() { Infinity = 111; });
function h1() { return Infinity; }
test((1/0), h1);
function h2() { return (1/0); }
test((1/0), h2);
function h3() { return (1/0) == (1/0); }
test(true, h3);
function h4() { return (1/0) == Infinity; }
test(true, h4);
function h5() { return Infinity == (1/0); }
test(true, h5);
function h6() { return "" + Infinity; }
test("Infinity", h6);
function h7() { return (1/0) === (1/0); }
test(true, h7);
function h8() { return (1/0) === Infinity; }
test(true, h8);
function h9() { return Infinity === (1/0); }
test(true, h9);
// -------------
function k1() { return this.Infinity; }
testThrows(k1);
function k2() { return (1/0); }
test((1/0), k2);
function k3() { return (1/0) == (1/0); }
test(true, k3);
function k4() { return (1/0) == this.Infinity; }
testThrows(k4);
function k5() { return this.Infinity == (1/0); }
testThrows(k5);
function k6() { return "" + this.Infinity; }
testThrows(k6);
function k7() { return (1/0) === (1/0); }
test(true, k7);
function k8() { return (1/0) === this.Infinity; }
testThrows(k8);
function k9() { return this.Infinity === (1/0); }
testThrows(k9);
// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
function test(expected, f) {
assertEquals(expected, f());
assertEquals(expected, f());
%OptimizeFunctionOnNextCall(f);
assertEquals(expected, f());
assertEquals(expected, f());
}
function f1() { return Infinity; }
test((1/0), f1);
function f2() { return (1/0); }
test((1/0), f2);
function f3() { return (1/0) == (1/0); }
test(true, f3);
function f4() { return (1/0) == Infinity; }
test(true, f4);
function f5() { return Infinity == (1/0); }
test(true, f5);
function f6() { return "" + Infinity; }
test("Infinity", f6);
function f7() { return (1/0) === (1/0); }
test(true, f7);
function f8() { return (1/0) === Infinity; }
test(true, f8);
function f9() { return Infinity === (1/0); }
test(true, f9);
delete Infinity;
function g1() { return Infinity; }
test((1/0), g1);
function g2() { return (1/0); }
test((1/0), g2);
function g3() { return (1/0) == (1/0); }
test(true, g3);
function g4() { return (1/0) == Infinity; }
test(true, g4);
function g5() { return Infinity == (1/0); }
test(true, g5);
function g6() { return "" + Infinity; }
test("Infinity", g6);
function g7() { return (1/0) === (1/0); }
test(true, g7);
function g8() { return (1/0) === Infinity; }
test(true, g8);
function g9() { return Infinity === (1/0); }
test(true, g9);
Infinity = 111;
function h1() { return Infinity; }
test((1/0), h1);
function h2() { return (1/0); }
test((1/0), h2);
function h3() { return (1/0) == (1/0); }
test(true, h3);
function h4() { return (1/0) == Infinity; }
test(true, h4);
function h5() { return Infinity == (1/0); }
test(true, h5);
function h6() { return "" + Infinity; }
test("Infinity", h6);
function h7() { return (1/0) === (1/0); }
test(true, h7);
function h8() { return (1/0) === Infinity; }
test(true, h8);
function h9() { return Infinity === (1/0); }
test(true, h9);
// -------------
function k1() { return this.Infinity; }
test((1/0), k1);
function k2() { return (1/0); }
test((1/0), k2);
function k3() { return (1/0) == (1/0); }
test(true, k3);
function k4() { return (1/0) == this.Infinity; }
test(true, k4);
function k5() { return this.Infinity == (1/0); }
test(true, k5);
function k6() { return "" + this.Infinity; }
test("Infinity", k6);
function k7() { return (1/0) === (1/0); }
test(true, k7);
function k8() { return (1/0) === this.Infinity; }
test(true, k8);
function k9() { return this.Infinity === (1/0); }
test(true, k9);
// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
"use strict";
function test(expected, f) {
assertEquals(expected, f());
assertEquals(expected, f());
%OptimizeFunctionOnNextCall(f);
assertEquals(expected, f());
assertEquals(expected, f());
}
function testThrows(f) {
assertThrows(f);
assertThrows(f);
%OptimizeFunctionOnNextCall(f);
assertThrows(f);
assertThrows(f);
}
function f1() { return NaN; }
test((0/0), f1);
function f2() { return (0/0); }
test((0/0), f2);
function f3() { return (0/0) == (0/0); }
test(false, f3);
function f4() { return (0/0) == NaN; }
test(false, f4);
function f5() { return NaN == (0/0); }
test(false, f5);
function f6() { return "" + NaN; }
test("NaN", f6);
function f7() { return (0/0) === (0/0); }
test(false, f7);
function f8() { return (0/0) === NaN; }
test(false, f8);
function f9() { return NaN === (0/0); }
test(false, f9);
// ----
function g1() { return NaN; }
test((0/0), g1);
function g2() { return (0/0); }
test((0/0), g2);
function g3() { return (0/0) == (0/0); }
test(false, g3);
function g4() { return (0/0) == NaN; }
test(false, g4);
function g5() { return NaN == (0/0); }
test(false, g5);
function g6() { return "" + NaN; }
test("NaN", g6);
function g7() { return (0/0) === (0/0); }
test(false, g7);
function g8() { return (0/0) === NaN; }
test(false, g8);
function g9() { return NaN === (0/0); }
test(false, g9);
testThrows(function() { NaN = 111; });
function h1() { return NaN; }
test((0/0), h1);
function h2() { return (0/0); }
test((0/0), h2);
function h3() { return (0/0) == (0/0); }
test(false, h3);
function h4() { return (0/0) == NaN; }
test(false, h4);
function h5() { return NaN == (0/0); }
test(false, h5);
function h6() { return "" + NaN; }
test("NaN", h6);
function h7() { return (0/0) === (0/0); }
test(false, h7);
function h8() { return (0/0) === NaN; }
test(false, h8);
function h9() { return NaN === (0/0); }
test(false, h9);
// -------------
function k1() { return this.NaN; }
testThrows(k1);
function k2() { return (0/0); }
test((0/0), k2);
function k3() { return (0/0) == (0/0); }
test(false, k3);
function k4() { return (0/0) == this.NaN; }
testThrows(k4);
function k5() { return this.NaN == (0/0); }
testThrows(k5);
function k6() { return "" + this.NaN; }
testThrows(k6);
function k7() { return (0/0) === (0/0); }
test(false, k7);
function k8() { return (0/0) === this.NaN; }
testThrows(k8);
function k9() { return this.NaN === (0/0); }
testThrows(k9);
// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
function test(expected, f) {
assertEquals(expected, f());
assertEquals(expected, f());
%OptimizeFunctionOnNextCall(f);
assertEquals(expected, f());
assertEquals(expected, f());
}
function f1() { return NaN; }
test((0/0), f1);
function f2() { return (0/0); }
test((0/0), f2);
function f3() { return (0/0) == (0/0); }
test(false, f3);
function f4() { return (0/0) == NaN; }
test(false, f4);
function f5() { return NaN == (0/0); }
test(false, f5);
function f6() { return "" + NaN; }
test("NaN", f6);
function f7() { return (0/0) === (0/0); }
test(false, f7);
function f8() { return (0/0) === NaN; }
test(false, f8);
function f9() { return NaN === (0/0); }
test(false, f9);
delete NaN;
function g1() { return NaN; }
test((0/0), g1);
function g2() { return (0/0); }
test((0/0), g2);
function g3() { return (0/0) == (0/0); }
test(false, g3);
function g4() { return (0/0) == NaN; }
test(false, g4);
function g5() { return NaN == (0/0); }
test(false, g5);
function g6() { return "" + NaN; }
test("NaN", g6);
function g7() { return (0/0) === (0/0); }
test(false, g7);
function g8() { return (0/0) === NaN; }
test(false, g8);
function g9() { return NaN === (0/0); }
test(false, g9);
NaN = 111;
function h1() { return NaN; }
test((0/0), h1);
function h2() { return (0/0); }
test((0/0), h2);
function h3() { return (0/0) == (0/0); }
test(false, h3);
function h4() { return (0/0) == NaN; }
test(false, h4);
function h5() { return NaN == (0/0); }
test(false, h5);
function h6() { return "" + NaN; }
test("NaN", h6);
function h7() { return (0/0) === (0/0); }
test(false, h7);
function h8() { return (0/0) === NaN; }
test(false, h8);
function h9() { return NaN === (0/0); }
test(false, h9);
// -------------
function k1() { return this.NaN; }
test((0/0), k1);
function k2() { return (0/0); }
test((0/0), k2);
function k3() { return (0/0) == (0/0); }
test(false, k3);
function k4() { return (0/0) == this.NaN; }
test(false, k4);
function k5() { return this.NaN == (0/0); }
test(false, k5);
function k6() { return "" + this.NaN; }
test("NaN", k6);
function k7() { return (0/0) === (0/0); }
test(false, k7);
function k8() { return (0/0) === this.NaN; }
test(false, k8);
function k9() { return this.NaN === (0/0); }
test(false, k9);
// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
"use strict";
function test(expected, f) {
assertEquals(expected, f());
assertEquals(expected, f());
%OptimizeFunctionOnNextCall(f);
assertEquals(expected, f());
assertEquals(expected, f());
}
function testThrows(f) {
assertThrows(f);
assertThrows(f);
%OptimizeFunctionOnNextCall(f);
assertThrows(f);
assertThrows(f);
}
function f1() { return undefined; }
test(void 0, f1);
function f2() { return void 0; }
test(void 0, f2);
function f3() { return void 0 == void 0; }
test(true, f3);
function f4() { return void 0 == undefined; }
test(true, f4);
function f5() { return undefined == void 0; }
test(true, f5);
function f6() { return "" + undefined; }
test("undefined", f6);
function f7() { return void 0 === void 0; }
test(true, f7);
function f8() { return void 0 === undefined; }
test(true, f8);
function f9() { return undefined === void 0; }
test(true, f9);
function g1() { return this; }
test(void 0, g1);
function g2() { return void 0; }
test(void 0, g2);
function g3() { return void 0 == void 0; }
test(true, g3);
function g4() { return void 0 == this; }
test(true, g4);
function g5() { return this == void 0; }
test(true, g5);
function g6() { return "" + this; }
test("undefined", g6);
function g7() { return void 0 === void 0; }
test(true, g7);
function g8() { return void 0 === this; }
test(true, g8);
function g9() { return this === void 0; }
test(true, g9);
testThrows(function() { undefined = 111; });
function h1() { return undefined; }
test(void 0, h1);
function h2() { return void 0; }
test(void 0, h2);
function h3() { return void 0 == void 0; }
test(true, h3);
function h4() { return void 0 == undefined; }
test(true, h4);
function h5() { return undefined == void 0; }
test(true, h5);
function h6() { return "" + undefined; }
test("undefined", h6);
function h7() { return void 0 === void 0; }
test(true, h7);
function h8() { return void 0 === undefined; }
test(true, h8);
function h9() { return undefined === void 0; }
test(true, h9);
// -------------
function k1() { return this; }
test(void 0, k1);
function k2() { return void 0; }
test(void 0, k2);
function k3() { return this === undefined; }
test(true, k3);
function k4() { return void 0 === this; }
test(true, k4);
function k5() { return this === void 0; }
test(true, k5);
function k6() { return "" + this; }
test("undefined", k6);
function k7() { return void 0 === void 0; }
test(true, k7);
function k8() { return void 0 === this; }
test(true, k8);
function k9() { return this === void 0; }
test(true, k9);
// -------------
function m1() { return this.undefined; }
testThrows(m1);
function m2() { return void 0; }
test(void 0, m2);
function m3() { return this === undefined; }
test(true, m3);
function m4() { return void 0 === this.undefined; }
testThrows(m4);
function m5() { return this.undefined == void 0; }
testThrows(m5);
function m6() { return "" + this.undefined; }
testThrows(m6);
function m7() { return void 0 === void 0; }
test(true, m7);
function m8() { return void 0 === this.undefined; }
testThrows(m8);
function m9() { return this.undefined === void 0; }
testThrows(m9);
// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Flags: --allow-natives-syntax
function test(expected, f) {
assertEquals(expected, f());
assertEquals(expected, f());
%OptimizeFunctionOnNextCall(f);
assertEquals(expected, f());
assertEquals(expected, f());
}
function testThrows(f) {
assertThrows(f);
assertThrows(f);
%OptimizeFunctionOnNextCall(f);
assertThrows(f);
assertThrows(f);
}
function f1() { return undefined; }
test(void 0, f1);
function f2() { return void 0; }
test(void 0, f2);
function f3() { return void 0 == void 0; }
test(true, f3);
function f4() { return void 0 == undefined; }
test(true, f4);
function f5() { return undefined == void 0; }
test(true, f5);
function f6() { return "" + undefined; }
test("undefined", f6);
function f7() { return void 0 === void 0; }
test(true, f7);
function f8() { return void 0 === undefined; }
test(true, f8);
function f9() { return undefined === void 0; }
test(true, f9);
delete undefined;
function g1() { return undefined; }
test(void 0, g1);
function g2() { return void 0; }
test(void 0, g2);
function g3() { return void 0 == void 0; }
test(true, g3);
function g4() { return void 0 == undefined; }
test(true, g4);
function g5() { return undefined == void 0; }
test(true, g5);
function g6() { return "" + undefined; }
test("undefined", g6);
function g7() { return void 0 === void 0; }
test(true, g7);
function g8() { return void 0 === undefined; }
test(true, g8);
function g9() { return undefined === void 0; }
test(true, g9);
undefined = 111;
function h1() { return undefined; }
test(void 0, h1);
function h2() { return void 0; }
test(void 0, h2);
function h3() { return void 0 == void 0; }
test(true, h3);
function h4() { return void 0 == undefined; }
test(true, h4);
function h5() { return undefined == void 0; }
test(true, h5);
function h6() { return "" + undefined; }
test("undefined", h6);
function h7() { return void 0 === void 0; }
test(true, h7);
function h8() { return void 0 === undefined; }
test(true, h8);
function h9() { return undefined === void 0; }
test(true, h9);
// -------------
function k1() { return this.undefined; }
test(void 0, k1);
function k2() { return void 0; }
test(void 0, k2);
function k3() { return void 0 == void 0; }
test(true, k3);
function k4() { return void 0 == this.undefined; }
test(true, k4);
function k5() { return this.undefined == void 0; }
test(true, k5);
function k6() { return "" + this.undefined; }
test("undefined", k6);
function k7() { return void 0 === void 0; }
test(true, k7);
function k8() { return void 0 === this.undefined; }
test(true, k8);
function k9() { return this.undefined === void 0; }
test(true, k9);
// -------------
function m1() { return undefined.x; }
testThrows(m1);
function m2() { return undefined.undefined; }
testThrows(m2);
function m3() { return (void 0).x; }
testThrows(m3);
function m4() { return (void 0).undefined; }
testThrows(m4);
......@@ -852,6 +852,43 @@ TEST_F(JSTypedLoweringTest, JSStorePropertyToExternalTypedArrayWithSafeKey) {
}
}
TEST_F(JSTypedLoweringTest, JSLoadNamedGlobalConstants) {
Handle<String> names[] = {
Handle<String>(isolate()->heap()->undefined_string(), isolate()),
Handle<String>(isolate()->heap()->infinity_string(), isolate()),
Handle<String>(isolate()->heap()->nan_string(), isolate()) // --
};
Matcher<Node*> matches[] = {
IsHeapConstant(Unique<HeapObject>::CreateImmovable(
Handle<HeapObject>(isolate()->heap()->undefined_value(), isolate()))),
IsNumberConstant(std::numeric_limits<double>::infinity()),
IsNumberConstant(IsNaN()) // --
};
VectorSlotPair feedback(Handle<TypeFeedbackVector>::null(),
FeedbackVectorICSlot::Invalid());
Node* global = Parameter(Type::GlobalObject());
Node* context = UndefinedConstant();
Node* effect = graph()->start();
Node* control = graph()->start();
for (size_t i = 0; i < arraysize(names); i++) {
Unique<Name> name = Unique<Name>::CreateImmovable(names[i]);
Node* node = graph()->NewNode(javascript()->LoadNamed(name, feedback),
global, context);
if (FLAG_turbo_deoptimization) {
node->AppendInput(zone(), EmptyFrameState());
}
node->AppendInput(zone(), effect);
node->AppendInput(zone(), control);
Reduction r = Reduce(node);
EXPECT_THAT(r.replacement(), matches[i]);
}
}
} // namespace compiler
} // namespace internal
} // namespace v8
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