Commit 21532ddf authored by verwaest@chromium.org's avatar verwaest@chromium.org

Reland ArrayPop / ArrayPush.

R=mvstanton@chromium.org

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

git-svn-id: http://v8.googlecode.com/svn/branches/bleeding_edge@18814 ce2b1a6d-e550-0410-aec6-3dcde31c8c00
parent 2311b678
......@@ -427,6 +427,21 @@ void CallDescriptors::InitializeForIsolate(Isolate* isolate) {
descriptor->param_representations_ = representations;
descriptor->platform_specific_descriptor_ = &noInlineDescriptor;
}
{
CallInterfaceDescriptor* descriptor =
isolate->call_descriptor(Isolate::CallHandler);
static Register registers[] = { cp, // context
r0, // receiver
};
static Representation representations[] = {
Representation::Tagged(), // context
Representation::Tagged(), // receiver
};
descriptor->register_param_count_ = 2;
descriptor->register_params_ = registers;
descriptor->param_representations_ = representations;
descriptor->platform_specific_descriptor_ = &default_descriptor;
}
}
......
......@@ -1601,79 +1601,6 @@ Handle<Code> CallStubCompiler::CompileCallField(Handle<JSObject> object,
}
Handle<Code> CallStubCompiler::CompileArrayPopCall(
Handle<Object> object,
Handle<JSObject> holder,
Handle<Cell> cell,
Handle<JSFunction> function,
Handle<String> name,
Code::StubType type) {
// If object is not an array or is observed or sealed, bail out to regular
// call.
if (!object->IsJSArray() ||
!cell.is_null() ||
Handle<JSArray>::cast(object)->map()->is_observed() ||
!Handle<JSArray>::cast(object)->map()->is_extensible()) {
return Handle<Code>::null();
}
Label miss, return_undefined, call_builtin;
Register receiver = r0;
Register scratch = r1;
Register elements = r3;
HandlerFrontendHeader(object, holder, name, RECEIVER_MAP_CHECK, &miss);
// Get the elements array of the object.
__ ldr(elements, FieldMemOperand(receiver, JSArray::kElementsOffset));
// Check that the elements are in fast mode and writable.
__ CheckMap(elements,
scratch,
Heap::kFixedArrayMapRootIndex,
&call_builtin,
DONT_DO_SMI_CHECK);
// Get the array's length into r4 and calculate new length.
__ ldr(r4, FieldMemOperand(receiver, JSArray::kLengthOffset));
__ sub(r4, r4, Operand(Smi::FromInt(1)), SetCC);
__ b(lt, &return_undefined);
// Get the last element.
__ LoadRoot(r6, Heap::kTheHoleValueRootIndex);
// We can't address the last element in one operation. Compute the more
// expensive shift first, and use an offset later on.
__ add(elements, elements, Operand::PointerOffsetFromSmiKey(r4));
__ ldr(scratch, FieldMemOperand(elements, FixedArray::kHeaderSize));
__ cmp(scratch, r6);
__ b(eq, &call_builtin);
// Set the array's length.
__ str(r4, FieldMemOperand(receiver, JSArray::kLengthOffset));
// Fill with the hole.
__ str(r6, FieldMemOperand(elements, FixedArray::kHeaderSize));
const int argc = arguments().immediate();
__ Drop(argc + 1);
__ mov(r0, scratch);
__ Ret();
__ bind(&return_undefined);
__ LoadRoot(r0, Heap::kUndefinedValueRootIndex);
__ Drop(argc + 1);
__ Ret();
__ bind(&call_builtin);
__ TailCallExternalReference(
ExternalReference(Builtins::c_ArrayPop, isolate()), argc + 1, 1);
HandlerFrontendFooter(&miss);
// Return the generated code.
return GetCode(type, name);
}
Handle<Code> CallStubCompiler::CompileFastApiCall(
const CallOptimization& optimization,
Handle<Object> object,
......
......@@ -7612,6 +7612,90 @@ bool HOptimizedGraphBuilder::TryInlineBuiltinMethodCall(
return true;
}
break;
case kArrayPop: {
if (!expr->IsMonomorphic() || expr->check_type() != RECEIVER_MAP_CHECK) {
return false;
}
if (receiver_map->instance_type() != JS_ARRAY_TYPE) return false;
ElementsKind elements_kind = receiver_map->elements_kind();
if (!IsFastElementsKind(elements_kind)) return false;
AddCheckConstantFunction(expr->holder(), receiver, receiver_map);
Drop(expr->arguments()->length());
HValue* result;
HValue* checked_object;
HValue* reduced_length;
HValue* receiver = Pop();
{ NoObservableSideEffectsScope scope(this);
checked_object = AddCheckMap(receiver, receiver_map);
HValue* elements = AddLoadElements(checked_object);
// Ensure that we aren't popping from a copy-on-write array.
if (IsFastSmiOrObjectElementsKind(elements_kind)) {
Add<HCheckMaps>(
elements, isolate()->factory()->fixed_array_map(), top_info());
}
HValue* length = Add<HLoadNamedField>(
checked_object, HObjectAccess::ForArrayLength(elements_kind));
reduced_length = AddUncasted<HSub>(length, graph()->GetConstant1());
HValue* bounds_check = Add<HBoundsCheck>(
graph()->GetConstant0(), length);
result = AddElementAccess(elements, reduced_length, NULL,
bounds_check, elements_kind, false);
Factory* factory = isolate()->factory();
double nan_double = FixedDoubleArray::hole_nan_as_double();
HValue* hole = IsFastSmiOrObjectElementsKind(elements_kind)
? Add<HConstant>(factory->the_hole_value())
: Add<HConstant>(nan_double);
if (IsFastSmiOrObjectElementsKind(elements_kind)) {
elements_kind = FAST_HOLEY_ELEMENTS;
}
AddElementAccess(
elements, reduced_length, hole, bounds_check, elements_kind, true);
}
Add<HStoreNamedField>(
checked_object, HObjectAccess::ForArrayLength(elements_kind),
reduced_length);
if (!ast_context()->IsEffect()) Push(result);
Add<HSimulate>(expr->id(), REMOVABLE_SIMULATE);
if (!ast_context()->IsEffect()) Drop(1);
ast_context()->ReturnValue(result);
return true;
}
case kArrayPush: {
if (!expr->IsMonomorphic() || expr->check_type() != RECEIVER_MAP_CHECK) {
return false;
}
if (receiver_map->instance_type() != JS_ARRAY_TYPE) return false;
ElementsKind elements_kind = receiver_map->elements_kind();
if (!IsFastElementsKind(elements_kind)) return false;
AddCheckConstantFunction(expr->holder(), receiver, receiver_map);
HValue* op_vals[] = {
context(),
// Receiver.
environment()->ExpressionStackAt(expr->arguments()->length())
};
const int argc = expr->arguments()->length();
// Includes receiver.
PushArgumentsFromEnvironment(argc + 1);
CallInterfaceDescriptor* descriptor =
isolate()->call_descriptor(Isolate::CallHandler);
ArrayPushStub stub(receiver_map->elements_kind(), argc);
Handle<Code> code = stub.GetCode(isolate());
HConstant* code_value = Add<HConstant>(code);
ASSERT((sizeof(op_vals) / kPointerSize) ==
descriptor->environment_length());
HInstruction* call = New<HCallWithDescriptor>(
code_value, argc + 1, descriptor,
Vector<HValue*>(op_vals, descriptor->environment_length()));
ast_context()->ReturnInstruction(call, expr->id());
return true;
}
default:
// Not yet supported for inlining.
break;
......
......@@ -421,6 +421,20 @@ void CallDescriptors::InitializeForIsolate(Isolate* isolate) {
descriptor->register_params_ = registers;
descriptor->param_representations_ = representations;
}
{
CallInterfaceDescriptor* descriptor =
isolate->call_descriptor(Isolate::CallHandler);
static Register registers[] = { esi, // context
edx, // receiver
};
static Representation representations[] = {
Representation::Tagged(), // context
Representation::Tagged(), // receiver
};
descriptor->register_param_count_ = 2;
descriptor->register_params_ = registers;
descriptor->param_representations_ = representations;
}
}
......
......@@ -1616,76 +1616,6 @@ Handle<Code> CallStubCompiler::CompileCallField(Handle<JSObject> object,
}
Handle<Code> CallStubCompiler::CompileArrayPopCall(
Handle<Object> object,
Handle<JSObject> holder,
Handle<Cell> cell,
Handle<JSFunction> function,
Handle<String> name,
Code::StubType type) {
// If object is not an array or is observed or sealed, bail out to regular
// call.
if (!object->IsJSArray() ||
!cell.is_null() ||
Handle<JSArray>::cast(object)->map()->is_observed() ||
!Handle<JSArray>::cast(object)->map()->is_extensible()) {
return Handle<Code>::null();
}
Label miss, return_undefined, call_builtin;
HandlerFrontendHeader(object, holder, name, RECEIVER_MAP_CHECK, &miss);
// Get the elements array of the object.
__ mov(ebx, FieldOperand(edx, JSArray::kElementsOffset));
// Check that the elements are in fast mode and writable.
__ cmp(FieldOperand(ebx, HeapObject::kMapOffset),
Immediate(factory()->fixed_array_map()));
__ j(not_equal, &call_builtin);
// Get the array's length into ecx and calculate new length.
__ mov(ecx, FieldOperand(edx, JSArray::kLengthOffset));
__ sub(ecx, Immediate(Smi::FromInt(1)));
__ j(negative, &return_undefined);
// Get the last element.
STATIC_ASSERT(kSmiTagSize == 1);
STATIC_ASSERT(kSmiTag == 0);
__ mov(eax, FieldOperand(ebx,
ecx, times_half_pointer_size,
FixedArray::kHeaderSize));
__ cmp(eax, Immediate(factory()->the_hole_value()));
__ j(equal, &call_builtin);
// Set the array's length.
__ mov(FieldOperand(edx, JSArray::kLengthOffset), ecx);
// Fill with the hole.
__ mov(FieldOperand(ebx,
ecx, times_half_pointer_size,
FixedArray::kHeaderSize),
Immediate(factory()->the_hole_value()));
const int argc = arguments().immediate();
__ ret((argc + 1) * kPointerSize);
__ bind(&return_undefined);
__ mov(eax, Immediate(factory()->undefined_value()));
__ ret((argc + 1) * kPointerSize);
__ bind(&call_builtin);
__ TailCallExternalReference(
ExternalReference(Builtins::c_ArrayPop, isolate()),
argc + 1,
1);
HandlerFrontendFooter(&miss);
// Return the generated code.
return GetCode(type, name);
}
Handle<Code> CallStubCompiler::CompileFastApiCall(
const CallOptimization& optimization,
Handle<Object> object,
......
......@@ -1076,6 +1076,7 @@ class Isolate {
enum CallDescriptorKey {
KeyedCall,
NamedCall,
CallHandler,
ArgumentAdaptorCall,
NUMBER_OF_CALL_DESCRIPTORS
};
......
......@@ -419,6 +419,20 @@ void CallDescriptors::InitializeForIsolate(Isolate* isolate) {
descriptor->register_params_ = registers;
descriptor->param_representations_ = representations;
}
{
CallInterfaceDescriptor* descriptor =
isolate->call_descriptor(Isolate::CallHandler);
static Register registers[] = { cp, // context
a0, // receiver
};
static Representation representations[] = {
Representation::Tagged(), // context
Representation::Tagged(), // receiver
};
descriptor->register_param_count_ = 2;
descriptor->register_params_ = registers;
descriptor->param_representations_ = representations;
}
}
......
......@@ -1585,78 +1585,6 @@ Handle<Code> CallStubCompiler::CompileCallField(Handle<JSObject> object,
}
Handle<Code> CallStubCompiler::CompileArrayPopCall(
Handle<Object> object,
Handle<JSObject> holder,
Handle<Cell> cell,
Handle<JSFunction> function,
Handle<String> name,
Code::StubType type) {
// If object is not an array or is observed or sealed, bail out to regular
// call.
if (!object->IsJSArray() ||
!cell.is_null() ||
Handle<JSArray>::cast(object)->map()->is_observed() ||
!Handle<JSArray>::cast(object)->map()->is_extensible()) {
return Handle<Code>::null();
}
Label miss, return_undefined, call_builtin;
Register receiver = a0;
Register scratch = a1;
Register elements = a3;
HandlerFrontendHeader(object, holder, name, RECEIVER_MAP_CHECK, &miss);
// Get the elements array of the object.
__ lw(elements, FieldMemOperand(receiver, JSArray::kElementsOffset));
// Check that the elements are in fast mode and writable.
__ CheckMap(elements,
scratch,
Heap::kFixedArrayMapRootIndex,
&call_builtin,
DONT_DO_SMI_CHECK);
// Get the array's length into t0 and calculate new length.
__ lw(t0, FieldMemOperand(receiver, JSArray::kLengthOffset));
__ Subu(t0, t0, Operand(Smi::FromInt(1)));
__ Branch(&return_undefined, lt, t0, Operand(zero_reg));
// Get the last element.
__ LoadRoot(t2, Heap::kTheHoleValueRootIndex);
STATIC_ASSERT(kSmiTagSize == 1);
STATIC_ASSERT(kSmiTag == 0);
// We can't address the last element in one operation. Compute the more
// expensive shift first, and use an offset later on.
__ sll(t1, t0, kPointerSizeLog2 - kSmiTagSize);
__ Addu(elements, elements, t1);
__ lw(scratch, FieldMemOperand(elements, FixedArray::kHeaderSize));
__ Branch(&call_builtin, eq, scratch, Operand(t2));
// Set the array's length.
__ sw(t0, FieldMemOperand(receiver, JSArray::kLengthOffset));
// Fill with the hole.
__ sw(t2, FieldMemOperand(elements, FixedArray::kHeaderSize));
const int argc = arguments().immediate();
__ mov(v0, scratch);
__ DropAndRet(argc + 1);
__ bind(&return_undefined);
__ LoadRoot(v0, Heap::kUndefinedValueRootIndex);
__ DropAndRet(argc + 1);
__ bind(&call_builtin);
__ TailCallExternalReference(
ExternalReference(Builtins::c_ArrayPop, isolate()), argc + 1, 1);
HandlerFrontendFooter(&miss);
// Return the generated code.
return GetCode(type, name);
}
Handle<Code> CallStubCompiler::CompileFastApiCall(
const CallOptimization& optimization,
Handle<Object> object,
......
......@@ -1282,41 +1282,6 @@ void CallStubCompiler::GenerateJumpFunction(Handle<Object> object,
}
Handle<Code> CallStubCompiler::CompileArrayPushCall(
Handle<Object> object,
Handle<JSObject> holder,
Handle<Cell> cell,
Handle<JSFunction> function,
Handle<String> name,
Code::StubType type) {
// If object is not an array or is observed or sealed, bail out to regular
// call.
if (!object->IsJSArray() ||
!cell.is_null() ||
Handle<JSArray>::cast(object)->map()->is_observed() ||
!Handle<JSArray>::cast(object)->map()->is_extensible()) {
return Handle<Code>::null();
}
Label miss;
HandlerFrontendHeader(object, holder, name, RECEIVER_MAP_CHECK, &miss);
Handle<Map> map(Handle<JSArray>::cast(object)->map());
ElementsKind elements_kind = map->elements_kind();
const int argc = arguments().immediate();
ArrayPushStub stub(elements_kind, argc);
Handle<Code> code = stub.GetCode(isolate());
StubCompiler::GenerateTailCall(masm(), code);
HandlerFrontendFooter(&miss);
// Return the generated code.
return GetCode(type, name);
}
Handle<Code> CallStubCompiler::CompileCallConstant(
Handle<Object> object,
Handle<JSObject> holder,
......@@ -1913,13 +1878,6 @@ CallStubCompiler::CallStubCompiler(Isolate* isolate,
bool CallStubCompiler::HasCustomCallGenerator(Handle<JSFunction> function) {
if (function->shared()->HasBuiltinFunctionId()) {
BuiltinFunctionId id = function->shared()->builtin_function_id();
#define CALL_GENERATOR_CASE(name) if (id == k##name) return true;
CUSTOM_CALL_IC_GENERATORS(CALL_GENERATOR_CASE)
#undef CALL_GENERATOR_CASE
}
CallOptimization optimization(function);
return optimization.is_simple_api_call();
}
......@@ -1933,21 +1891,6 @@ Handle<Code> CallStubCompiler::CompileCustomCall(
Handle<String> fname,
Code::StubType type) {
ASSERT(HasCustomCallGenerator(function));
if (function->shared()->HasBuiltinFunctionId()) {
BuiltinFunctionId id = function->shared()->builtin_function_id();
#define CALL_GENERATOR_CASE(name) \
if (id == k##name) { \
return CallStubCompiler::Compile##name##Call(object, \
holder, \
cell, \
function, \
fname, \
type); \
}
CUSTOM_CALL_IC_GENERATORS(CALL_GENERATOR_CASE)
#undef CALL_GENERATOR_CASE
}
CallOptimization optimization(function);
ASSERT(optimization.is_simple_api_call());
return CompileFastApiCall(optimization,
......
......@@ -868,13 +868,6 @@ class KeyedStoreStubCompiler: public StoreStubCompiler {
};
// Subset of FUNCTIONS_WITH_ID_LIST with custom constant/global call
// IC stubs.
#define CUSTOM_CALL_IC_GENERATORS(V) \
V(ArrayPush) \
V(ArrayPop)
class CallStubCompiler: public StubCompiler {
public:
CallStubCompiler(Isolate* isolate,
......@@ -941,16 +934,6 @@ class CallStubCompiler: public StubCompiler {
Handle<String> name,
Code::StubType type);
#define DECLARE_CALL_GENERATOR(name) \
Handle<Code> Compile##name##Call(Handle<Object> object, \
Handle<JSObject> holder, \
Handle<Cell> cell, \
Handle<JSFunction> function, \
Handle<String> fname, \
Code::StubType type);
CUSTOM_CALL_IC_GENERATORS(DECLARE_CALL_GENERATOR)
#undef DECLARE_CALL_GENERATOR
Handle<Code> CompileFastApiCall(const CallOptimization& optimization,
Handle<Object> object,
Handle<JSObject> holder,
......
......@@ -418,6 +418,20 @@ void CallDescriptors::InitializeForIsolate(Isolate* isolate) {
descriptor->register_params_ = registers;
descriptor->param_representations_ = representations;
}
{
CallInterfaceDescriptor* descriptor =
isolate->call_descriptor(Isolate::CallHandler);
static Register registers[] = { rsi, // context
rdx, // receiver
};
static Representation representations[] = {
Representation::Tagged(), // context
Representation::Tagged(), // receiver
};
descriptor->register_param_count_ = 2;
descriptor->register_params_ = registers;
descriptor->param_representations_ = representations;
}
}
......
......@@ -1534,77 +1534,6 @@ Handle<Code> CallStubCompiler::CompileCallField(Handle<JSObject> object,
}
Handle<Code> CallStubCompiler::CompileArrayPopCall(
Handle<Object> object,
Handle<JSObject> holder,
Handle<Cell> cell,
Handle<JSFunction> function,
Handle<String> name,
Code::StubType type) {
// If object is not an array or is observed or sealed, bail out to regular
// call.
if (!object->IsJSArray() ||
!cell.is_null() ||
Handle<JSArray>::cast(object)->map()->is_observed() ||
!Handle<JSArray>::cast(object)->map()->is_extensible()) {
return Handle<Code>::null();
}
Label miss, return_undefined, call_builtin;
HandlerFrontendHeader(object, holder, name, RECEIVER_MAP_CHECK, &miss);
// Get the elements array of the object.
__ movp(rbx, FieldOperand(rdx, JSArray::kElementsOffset));
// Check that the elements are in fast mode and writable.
__ CompareRoot(FieldOperand(rbx, HeapObject::kMapOffset),
Heap::kFixedArrayMapRootIndex);
__ j(not_equal, &call_builtin);
// Get the array's length into rcx and calculate new length.
__ SmiToInteger32(rcx, FieldOperand(rdx, JSArray::kLengthOffset));
__ subl(rcx, Immediate(1));
__ j(negative, &return_undefined);
// Get the last element.
__ LoadRoot(r9, Heap::kTheHoleValueRootIndex);
__ movp(rax, FieldOperand(rbx,
rcx, times_pointer_size,
FixedArray::kHeaderSize));
// Check if element is already the hole.
__ cmpq(rax, r9);
// If so, call slow-case to also check prototypes for value.
__ j(equal, &call_builtin);
// Set the array's length.
__ Integer32ToSmiField(FieldOperand(rdx, JSArray::kLengthOffset), rcx);
// Fill with the hole and return original value.
__ movp(FieldOperand(rbx,
rcx, times_pointer_size,
FixedArray::kHeaderSize),
r9);
const int argc = arguments().immediate();
__ ret((argc + 1) * kPointerSize);
__ bind(&return_undefined);
__ LoadRoot(rax, Heap::kUndefinedValueRootIndex);
__ ret((argc + 1) * kPointerSize);
__ bind(&call_builtin);
__ TailCallExternalReference(
ExternalReference(Builtins::c_ArrayPop, isolate()),
argc + 1,
1);
HandlerFrontendFooter(&miss);
// Return the generated code.
return GetCode(type, name);
}
Handle<Code> CallStubCompiler::CompileFastApiCall(
const CallOptimization& optimization,
Handle<Object> object,
......
......@@ -25,6 +25,8 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Flags: --allow-natives-syntax
//
// Check pops with various number of arguments.
(function() {
var a = [];
......@@ -121,3 +123,18 @@
assertEquals(99 - i, x.pop(), i + 'th iteration');
}
})();
(function () {
function f(a, deopt) {
var v = a.pop() ? 1 : 2;
if (deopt) %DeoptimizeFunction(f);
return v;
}
var a = [true, true, true, true]
assertEquals(1, f(a, false));
assertEquals(1, f(a, false));
%OptimizeFunctionOnNextCall(f);
assertEquals(1, f(a, false));
assertEquals(1, f(a, true));
})();
......@@ -251,8 +251,7 @@ assertOptimized(shift_call);
Object.seal(obj);
assertThrows(function() { push_call(obj); }, TypeError);
assertThrows(function() { shift_call(obj); }, TypeError);
assertOptimized(push_call);
// shift() doesn't have a custom call generator, so deopt will occur.
assertUnoptimized(push_call);
assertUnoptimized(shift_call);
assertDoesNotThrow(function() { push_call(objControl); });
assertDoesNotThrow(function() { shift_call(objControl); });
......
// Copyright 2014 the V8 project authors. All rights reserved.
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// Flags: --allow-natives-syntax
var o = [6,7,8,9];
function f(b) {
var v = o.pop() + b;
return v;
}
assertEquals(10, f(1));
assertEquals(9, f(1));
assertEquals(8, f(1));
%OptimizeFunctionOnNextCall(f);
assertEquals("61", f("1"));
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