Commit 9362fa94 authored by Daniel Clifford's avatar Daniel Clifford Committed by Commit Bot

[builtins] Port Frame-related CSA functionality to Torque

Moving Frame-inspection functionality to Torque is a prerequisite
for porting the CSA-based arguments code, which is a great candidate
to simplify/cleanup with Torque.

Change-Id: I1f4cb94cb357aae5864c2e84f3bf5a07549b27f8
Reviewed-on: https://chromium-review.googlesource.com/c/1357050
Commit-Queue: Daniel Clifford <danno@chromium.org>
Reviewed-by: 's avatarJakob Gruber <jgruber@chromium.org>
Reviewed-by: 's avatarTobias Tebbi <tebbi@chromium.org>
Cr-Commit-Position: refs/heads/master@{#58106}
parent 03ea6754
......@@ -959,6 +959,7 @@ torque_files = [
"src/builtins/array-unshift.tq",
"src/builtins/collections.tq",
"src/builtins/data-view.tq",
"src/builtins/frames.tq",
"src/builtins/object.tq",
"src/builtins/object-fromentries.tq",
"src/builtins/iterator.tq",
......
......@@ -15,8 +15,8 @@ type Arguments constexpr 'CodeStubArguments*';
type void;
type never;
type Tagged generates 'TNode<Object>' constexpr 'Object*';
type Smi extends Tagged generates 'TNode<Smi>' constexpr 'Smi*';
type Tagged generates 'TNode<Object>' constexpr 'ObjectPtr';
type Smi extends Tagged generates 'TNode<Smi>' constexpr 'Smi';
type HeapObject extends Tagged generates 'TNode<HeapObject>';
type Object = Smi | HeapObject;
type int32 generates 'TNode<Int32T>' constexpr 'int32_t';
......@@ -128,6 +128,8 @@ type ToIntegerTruncationMode
constexpr 'CodeStubAssembler::ToIntegerTruncationMode';
type AllocationFlags constexpr 'AllocationFlags';
const kSmiTagSize: constexpr int31 generates 'kSmiTagSize';
const NO_ELEMENTS: constexpr ElementsKind generates 'NO_ELEMENTS';
const PACKED_SMI_ELEMENTS:
......@@ -345,6 +347,10 @@ extern transitioning runtime TransitionElementsKindWithKind(
extern transitioning runtime CreateDataProperty(implicit context: Context)(
JSReceiver, Object, Object);
extern macro LoadBufferObject(RawPtr, constexpr int32): Object;
extern macro LoadBufferPointer(RawPtr, constexpr int32): RawPtr;
extern macro LoadBufferSmi(RawPtr, constexpr int32): Smi;
extern macro LoadRoot(constexpr RootIndex): Object;
extern macro StoreRoot(constexpr RootIndex, Object): Object;
extern macro LoadAndUntagToWord32Root(constexpr RootIndex): int32;
......@@ -357,6 +363,9 @@ extern macro SmiLexicographicCompare(Smi, Smi): Smi;
extern runtime ReThrow(Context, Object): never;
extern runtime ThrowInvalidStringLength(Context): never;
extern operator '==' macro WordEqual(RawPtr, RawPtr): bool;
extern operator '!=' macro WordNotEqual(RawPtr, RawPtr): bool;
extern operator '<' macro Int32LessThan(int32, int32): bool;
extern operator '<' macro Uint32LessThan(uint32, uint32): bool;
extern operator '>' macro Int32GreaterThan(int32, int32): bool;
......@@ -433,21 +442,21 @@ extern operator '+' macro SmiAdd(Smi, Smi): Smi;
extern operator '-' macro SmiSub(Smi, Smi): Smi;
extern operator '&' macro SmiAnd(Smi, Smi): Smi;
extern operator '|' macro SmiOr(Smi, Smi): Smi;
extern operator '>>>' macro SmiShr(Smi, constexpr int31): Smi;
extern operator '<<' macro SmiShl(Smi, constexpr int31): Smi;
extern operator '>>' macro SmiSar(Smi, constexpr int31): Smi;
extern operator '+' macro IntPtrAdd(intptr, intptr): intptr;
extern operator '-' macro IntPtrSub(intptr, intptr): intptr;
extern operator '*' macro IntPtrMul(intptr, intptr): intptr;
extern operator '/' macro IntPtrDiv(intptr, intptr): intptr;
extern operator '<<' macro WordShl(intptr, intptr): intptr;
extern operator '>>' macro WordSar(intptr, intptr): intptr;
extern operator '&' macro WordAnd(intptr, intptr): intptr;
extern operator '|' macro WordOr(intptr, intptr): intptr;
extern operator '+' macro UintPtrAdd(uintptr, uintptr): uintptr;
extern operator '-' macro UintPtrSub(uintptr, uintptr): uintptr;
extern operator '>>>' macro WordShr(uintptr, uintptr): uintptr;
extern operator '<<' macro WordShl(uintptr, uintptr): uintptr;
extern operator '&' macro WordAnd(uintptr, uintptr): uintptr;
extern operator '|' macro WordOr(uintptr, uintptr): uintptr;
......@@ -487,6 +496,11 @@ macro Max(x: Number, y: Number): Number {
return NumberMax(x, y);
}
extern operator '<<' macro ConstexprUintPtrShl(
constexpr uintptr, constexpr int31): constexpr uintptr;
extern operator '>>>' macro ConstexprUintPtrShr(
constexpr uintptr, constexpr int31): constexpr uintptr;
extern macro SmiMax(Smi, Smi): Smi;
extern macro SmiMin(Smi, Smi): Smi;
......@@ -740,6 +754,7 @@ extern macro Int32Constant(constexpr int31): int31;
extern macro Int32Constant(constexpr int32): int32;
extern macro Float64Constant(constexpr int31): float64;
extern macro SmiConstant(constexpr int31): Smi;
extern macro SmiConstant(constexpr Smi): Smi;
extern macro BoolConstant(constexpr bool): bool;
extern macro StringConstant(constexpr string): String;
extern macro LanguageModeConstant(constexpr LanguageMode): LanguageMode;
......@@ -748,7 +763,10 @@ extern macro IntPtrConstant(constexpr NativeContextSlot): NativeContextSlot;
extern macro IntPtrConstant(constexpr intptr): intptr;
extern macro BitcastWordToTaggedSigned(intptr): Smi;
extern macro BitcastWordToTaggedSigned(uintptr): Smi;
extern macro BitcastWordToTagged(intptr): Object;
extern macro BitcastWordToTagged(uintptr): Object;
extern macro BitcastTaggedToWord(Tagged): intptr;
intrinsic %FromConstexpr<To: type, From: type>(b: From): To;
macro FromConstexpr<To: type, From: type>(o: From): To;
......@@ -767,6 +785,12 @@ FromConstexpr<intptr, constexpr int31>(i: constexpr int31): intptr {
FromConstexpr<intptr, constexpr int32>(i: constexpr int32): intptr {
return %FromConstexpr<intptr>(i);
}
FromConstexpr<intptr, constexpr intptr>(i: constexpr intptr): intptr {
return %FromConstexpr<intptr>(i);
}
FromConstexpr<uintptr, constexpr uintptr>(i: constexpr uintptr): uintptr {
return %FromConstexpr<uintptr>(i);
}
FromConstexpr<Smi, constexpr int31>(i: constexpr int31): Smi {
return %FromConstexpr<Smi>(i);
}
......@@ -785,6 +809,12 @@ FromConstexpr<Number, constexpr float64>(f: constexpr float64): Number {
FromConstexpr<Number, constexpr int31>(i: constexpr int31): Number {
return %FromConstexpr<Number>(i);
}
FromConstexpr<Number, constexpr Smi>(s: constexpr Smi): Number {
return SmiConstant(s);
}
FromConstexpr<Smi, constexpr Smi>(s: constexpr Smi): Smi {
return SmiConstant(s);
}
FromConstexpr<uint32, constexpr int31>(i: constexpr int31): uint32 {
return Unsigned(Int32Constant(i));
......@@ -890,6 +920,9 @@ Convert<Number>(ui: uintptr): Number {
Convert<uintptr>(d: float64): uintptr {
return ChangeFloat64ToUintPtr(d);
}
Convert<uintptr>(i: intptr): uintptr {
return Unsigned(i);
}
macro Convert<A: type>(r: RawPtr): A;
Convert<uintptr>(r: RawPtr): uintptr {
return Unsigned(r);
......@@ -1175,7 +1208,6 @@ extern macro IsCustomElementsReceiverInstanceType(int32): bool;
extern macro IsFastJSArrayWithNoCustomIteration(implicit context: Context)(
Object): bool;
extern macro Typeof(Object): Object;
extern macro LoadTargetFromFrame(): JSFunction;
// Return true iff number is NaN.
macro NumberIsNaN(number: Number): bool {
......
// Copyright 2018 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.
type FrameType extends Smi
generates 'TNode<Smi>' constexpr 'StackFrame::Type';
const ARGUMENTS_ADAPTOR_FRAME: constexpr FrameType
generates 'StackFrame::ARGUMENTS_ADAPTOR';
const STUB_FRAME: constexpr FrameType
generates 'StackFrame::STUB';
const kFrameTypeCount:
constexpr int31 generates 'StackFrame::NUMBER_OF_TYPES';
FromConstexpr<FrameType, constexpr FrameType>(t: constexpr FrameType):
FrameType {
// Note that althought FrameTypes sometimes masquerade as Smis (their
// LSB is a zero), they are not. For efficiency in storing them as a
// constant into a frame, they are simply the FrameType value shifted
// up by a single bit.
const i: constexpr uintptr = %RawConstexprCast<constexpr uintptr>(t)
<< kSmiTagSize;
return %RawObjectCast<FrameType>(BitcastWordToTaggedSigned(i));
}
Cast<FrameType>(o: Object): FrameType
labels CastError {
if (TaggedIsNotSmi(o)) goto CastError;
assert(
(Convert<uintptr>(BitcastTaggedToWord(o)) >>> kSmiTagSize) <
kFrameTypeCount);
return %RawObjectCast<FrameType>(o);
}
type FrameBase extends RawPtr
generates 'TNode<RawPtrT>' constexpr 'void*';
type StandardFrame extends FrameBase
generates 'TNode<RawPtrT>' constexpr 'void*';
type ArgumentsAdapterFrame extends FrameBase
generates 'TNode<RawPtrT>' constexpr 'void*';
type StubFrame extends FrameBase
generates 'TNode<RawPtrT>' constexpr 'void*';
type Frame = ArgumentsAdapterFrame | StandardFrame | StubFrame;
extern macro LoadFramePointer(): Frame;
extern macro LoadParentFramePointer(): Frame;
// Load values from a specified frame by given offset in bytes.
macro LoadObjectFromFrame(f: Frame, o: constexpr int32): Object {
return LoadBufferObject(f, o);
}
macro LoadPointerFromFrame(f: Frame, o: constexpr int32): RawPtr {
return LoadBufferPointer(f, o);
}
macro LoadSmiFromFrame(f: Frame, o: constexpr int32): Smi {
return LoadBufferSmi(f, o);
}
const kStandardFrameFunctionOffset: constexpr int31
generates 'StandardFrameConstants::kFunctionOffset';
operator '.function' macro LoadFunctionFromFrame(f: Frame): JSFunction {
// TODO(danno): Use RawObjectCast here in order to avoid passing the implicit
// context, since this accessor is used in legacy CSA code through
// LoadTargetFromFrame
const result: Object = LoadObjectFromFrame(f, kStandardFrameFunctionOffset);
return %RawObjectCast<JSFunction>(result);
}
const kStandardFrameCallerFPOffset: constexpr int31
generates 'StandardFrameConstants::kCallerFPOffset';
operator '.caller' macro LoadCallerFromFrame(f: Frame): Frame {
const result: RawPtr = LoadPointerFromFrame(f, kStandardFrameCallerFPOffset);
return %RawPointerCast<Frame>(result);
}
type ContextOrFrameType = Context | FrameType;
Cast<ContextOrFrameType>(implicit context: Context)(o: Object):
ContextOrFrameType
labels CastError {
typeswitch (o) {
case (c: Context): {
return c;
}
case (t: FrameType): {
return t;
}
case (Object): {
goto CastError;
}
}
}
const kStandardFrameContextOrFrameTypeOffset: constexpr int31
generates 'StandardFrameConstants::kContextOrFrameTypeOffset';
operator '.context_or_frame_type'
macro LoadContextOrFrameTypeFromFrame(implicit context: Context)(f: Frame):
ContextOrFrameType {
return UnsafeCast<ContextOrFrameType>(
LoadObjectFromFrame(f, kStandardFrameContextOrFrameTypeOffset));
}
const kArgumentsAdaptorFrameLengthOffset: constexpr int31
generates 'ArgumentsAdaptorFrameConstants::kLengthOffset';
operator '.length'
macro LoadLengthFromAdapterFrame(implicit context: Context)(
f: ArgumentsAdapterFrame): Smi {
return LoadSmiFromFrame(f, kArgumentsAdaptorFrameLengthOffset);
}
operator '==' macro FrameTypeEquals(f1: FrameType, f2: FrameType): bool {
return WordEqual(f1, f2);
}
macro Cast<A: type>(implicit context: Context)(o: Frame): A labels CastError;
Cast<StandardFrame>(implicit context: Context)(f: Frame):
StandardFrame labels CastError {
const o: HeapObject =
Cast<HeapObject>(f.context_or_frame_type) otherwise CastError;
// StandardFrames (which include interpreted and JIT-compiled frames),
// unlike other frame types, don't have their own type marker stored in
// the frame, but rather have the function's context stored where the
// type marker is stored for other frame types. From Torque, it would
// be quite expensive to do the test required to distinguish interpreter
// frames from JITted ones (and other StandardFrame types), so
// StandardFrame is the level of granularity support when iterating the
// stack from generated code.
// See the descriptions and frame layouts in src/frame-constants.h.
if (IsContext(o)) {
return %RawPointerCast<StandardFrame>(f);
}
goto CastError;
}
Cast<ArgumentsAdapterFrame>(implicit context: Context)(f: Frame):
ArgumentsAdapterFrame labels CastError {
const t: FrameType =
Cast<FrameType>(f.context_or_frame_type) otherwise CastError;
if (t == ARGUMENTS_ADAPTOR_FRAME) {
return %RawPointerCast<ArgumentsAdapterFrame>(f);
}
goto CastError;
}
// Load target function from the current JS frame.
// This is an alternative way of getting the target function in addition to
// Parameter(Descriptor::kJSTarget). The latter should be used near the
// beginning of builtin code while the target value is still in the register
// and the former should be used in slow paths in order to reduce register
// pressure on the fast path.
macro LoadTargetFromFrame(): JSFunction {
return LoadFramePointer().function;
}
......@@ -146,7 +146,7 @@ namespace typed_array {
// TODO(szuend): Check if a more involved thirdIndex calculation is
// worth it for very large arrays.
const thirdIndex: Smi = from + ((to - from) >>> 1);
const thirdIndex: Smi = from + ((to - from) >> 1);
if (IsDetachedBuffer(array.buffer)) goto Detached;
......
......@@ -1307,22 +1307,11 @@ void CodeStubAssembler::BranchIfToBooleanIsTrue(Node* value, Label* if_true,
}
}
Node* CodeStubAssembler::LoadFromFrame(int offset, MachineType rep) {
Node* frame_pointer = LoadFramePointer();
return Load(rep, frame_pointer, IntPtrConstant(offset));
}
Node* CodeStubAssembler::LoadFromParentFrame(int offset, MachineType rep) {
Node* frame_pointer = LoadParentFramePointer();
return Load(rep, frame_pointer, IntPtrConstant(offset));
}
TNode<JSFunction> CodeStubAssembler::LoadTargetFromFrame() {
DCHECK(IsJSFunctionCall());
return CAST(LoadFromFrame(StandardFrameConstants::kFunctionOffset,
MachineType::TaggedPointer()));
}
Node* CodeStubAssembler::LoadBufferObject(Node* buffer, int offset,
MachineType rep) {
return Load(rep, buffer, IntPtrConstant(offset));
......
......@@ -16,6 +16,7 @@
#include "src/objects.h"
#include "src/objects/arguments.h"
#include "src/objects/bigint.h"
#include "src/objects/shared-function-info.h"
#include "src/objects/smi.h"
#include "src/roots.h"
......@@ -352,6 +353,9 @@ class V8_EXPORT_PRIVATE CodeStubAssembler
SmiAboveOrEqual)
#undef PARAMETER_BINOP
uintptr_t ConstexprUintPtrShl(uintptr_t a, int32_t b) { return a << b; }
uintptr_t ConstexprUintPtrShr(uintptr_t a, int32_t b) { return a >> b; }
TNode<Object> NoContextConstant();
#define HEAP_CONSTANT_ACCESSOR(rootIndexName, rootAccessorName, name) \
......@@ -460,6 +464,12 @@ class V8_EXPORT_PRIVATE CodeStubAssembler
BitcastTaggedToWord(SmiConstant(-1))));
}
TNode<Smi> SmiSar(TNode<Smi> a, int shift) {
return BitcastWordToTaggedSigned(
WordAnd(WordSar(BitcastTaggedToWord(a), shift),
BitcastTaggedToWord(SmiConstant(-1))));
}
Node* WordOrSmiShl(Node* a, int shift, ParameterMode mode) {
if (mode == SMI_PARAMETERS) {
return SmiShl(CAST(a), shift);
......@@ -713,23 +723,20 @@ class V8_EXPORT_PRIVATE CodeStubAssembler
// Branches to {if_true} when Debug::ExecutionMode is DebugInfo::kSideEffect.
void GotoIfDebugExecutionModeChecksSideEffects(Label* if_true);
// Load value from current frame by given offset in bytes.
Node* LoadFromFrame(int offset, MachineType rep = MachineType::AnyTagged());
// Load value from current parent frame by given offset in bytes.
Node* LoadFromParentFrame(int offset,
MachineType rep = MachineType::AnyTagged());
// Load target function from the current JS frame.
// This is an alternative way of getting the target function in addition to
// Parameter(Descriptor::kJSTarget). The latter should be used near the
// beginning of builtin code while the target value is still in the register
// and the former should be used in slow paths in order to reduce register
// pressure on the fast path.
TNode<JSFunction> LoadTargetFromFrame();
// Load an object pointer from a buffer that isn't in the heap.
Node* LoadBufferObject(Node* buffer, int offset,
MachineType rep = MachineType::AnyTagged());
TNode<RawPtrT> LoadBufferPointer(TNode<RawPtrT> buffer, int offset) {
return UncheckedCast<RawPtrT>(
LoadBufferObject(buffer, offset, MachineType::Pointer()));
}
TNode<Smi> LoadBufferSmi(TNode<RawPtrT> buffer, int offset) {
return CAST(LoadBufferObject(buffer, offset, MachineType::TaggedSigned()));
}
// Load a field from an object on the heap.
Node* LoadObjectField(SloppyTNode<HeapObject> object, int offset,
MachineType rep);
......@@ -1129,6 +1136,19 @@ class V8_EXPORT_PRIVATE CodeStubAssembler
return LoadObjectField(function, JSFunction::kPrototypeOrInitialMapOffset);
}
TNode<SharedFunctionInfo> LoadJSFunctionSharedFunctionInfo(
TNode<JSFunction> function) {
return CAST(
LoadObjectField(function, JSFunction::kSharedFunctionInfoOffset));
}
TNode<Int32T> LoadSharedFunctionInfoFormalParameterCount(
TNode<SharedFunctionInfo> function) {
return TNode<Int32T>::UncheckedCast(LoadObjectField(
function, SharedFunctionInfo::kFormalParameterCountOffset,
MachineType::Uint16()));
}
void StoreObjectByteNoWriteBarrier(TNode<HeapObject> object, int offset,
TNode<Word32T> value);
......
......@@ -71,8 +71,8 @@ class CommonFrameConstants : public AllStatic {
-(kCPSlotSize + kContextOrFrameTypeSize);
};
// StandardFrames are used for interpreted, full-codegen and optimized
// JavaScript frames. They always have a context below the saved fp/constant
// StandardFrames are used for interpreted and optimized JavaScript
// frames. They always have a context below the saved fp/constant
// pool and below that the JSFunction of the executing function.
//
// slot JS frame
......
......@@ -226,6 +226,8 @@ void CSAGenerator::EmitInstruction(const CallIntrinsicInstruction& instruction,
"String or Number");
} else if (return_type->IsSubtypeOf(TypeOracle::GetIntPtrType())) {
out_ << "ca_.IntPtrConstant";
} else if (return_type->IsSubtypeOf(TypeOracle::GetUIntPtrType())) {
out_ << "ca_.UintPtrConstant";
} else if (return_type->IsSubtypeOf(TypeOracle::GetInt32Type())) {
out_ << "ca_.Int32Constant";
} else {
......
......@@ -1808,6 +1808,12 @@ VisitResult ImplementationVisitor::GenerateCall(
"%RawConstexprCast must take a single parameter with constexpr "
"type");
}
if (!return_type->IsConstexpr()) {
std::stringstream s;
s << *return_type
<< " return type for %RawConstexprCast is not constexpr";
ReportError(s.str());
}
std::stringstream result;
result << "static_cast<" << return_type->GetGeneratedTypeName() << ">(";
result << constexpr_arguments[0];
......
......@@ -124,6 +124,10 @@ class TypeOracle : public ContextualClass<TypeOracle> {
return Get().GetBuiltinType(INTPTR_TYPE_STRING);
}
static const Type* GetUIntPtrType() {
return Get().GetBuiltinType(UINTPTR_TYPE_STRING);
}
static const Type* GetInt32Type() {
return Get().GetBuiltinType(INT32_TYPE_STRING);
}
......
......@@ -34,6 +34,7 @@ static const char* const STRING_TYPE_STRING = "String";
static const char* const NUMBER_TYPE_STRING = "Number";
static const char* const CODE_TYPE_STRING = "Code";
static const char* const INTPTR_TYPE_STRING = "intptr";
static const char* const UINTPTR_TYPE_STRING = "uintptr";
static const char* const INT32_TYPE_STRING = "int32";
static const char* const CONST_INT31_TYPE_STRING = "constexpr int31";
static const char* const CONST_INT32_TYPE_STRING = "constexpr int32";
......
......@@ -375,6 +375,22 @@ TEST(TestLookup) {
ft.Call();
}
TEST(TestFrame1) {
CcTest::InitializeVM();
Isolate* isolate(CcTest::i_isolate());
i::HandleScope scope(isolate);
Handle<Context> context =
Utils::OpenHandle(*v8::Isolate::GetCurrent()->GetCurrentContext());
CodeAssemblerTester asm_tester(isolate);
TestTorqueAssembler m(asm_tester.state());
{
m.TestFrame1(m.UncheckedCast<Context>(m.HeapConstant(context)));
m.Return(m.UndefinedConstant());
}
FunctionTester ft(asm_tester.GenerateCode(), 0);
ft.Call();
}
} // namespace compiler
} // namespace internal
} // namespace v8
......@@ -670,4 +670,23 @@ namespace test {
}
label Fail {}
}
macro TestFrame1(implicit context: Context)() {
const f: Frame = LoadFramePointer();
const frameType: FrameType =
Cast<FrameType>(f.context_or_frame_type) otherwise unreachable;
assert(frameType == STUB_FRAME);
assert(f.caller == LoadParentFramePointer());
typeswitch (f) {
case (f: StandardFrame): {
unreachable;
}
case (f: ArgumentsAdapterFrame): {
unreachable;
}
case (f: StubFrame): {
}
}
}
}
......@@ -676,7 +676,7 @@ namespace array {
// Find pivot insertion point.
while (left < right) {
const mid: Smi = left + ((right - left) >>> 1);
const mid: Smi = left + ((right - left) >> 1);
const midElement: Object =
CallLoad(context, sortState, load, elements, mid)
otherwise Bailout;
......@@ -999,7 +999,7 @@ namespace array {
// a[base + lastOfs - 1] < key <= a[base + offset].
lastOfs++;
while (lastOfs < offset) {
const m: Smi = lastOfs + ((offset - lastOfs) >>> 1);
const m: Smi = lastOfs + ((offset - lastOfs) >> 1);
const baseMElement: Object = CallLoad(
context, sortState, load,
......@@ -1120,7 +1120,7 @@ namespace array {
// a[base + lastOfs - 1] < key <= a[base + ofs].
lastOfs++;
while (lastOfs < offset) {
const m: Smi = lastOfs + ((offset - lastOfs) >>> 1);
const m: Smi = lastOfs + ((offset - lastOfs) >> 1);
const baseMElement: Object = CallLoad(
context, sortState, load,
......@@ -1531,7 +1531,7 @@ namespace array {
assert(n >= 0);
while (n >= 64) {
r = r | (n & 1);
n = n >>> 1;
n = n >> 1;
}
const minRunLength: Smi = n + r;
......
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