Skip to content

Commit f5296a1

Browse files
authored
Merge pull request chakra-core#6662 from rhuanjl/jitGenerators
Enable Jit for Generators, Asyncs and Async Generators
2 parents 743f8d0 + 191c760 commit f5296a1

16 files changed

+206
-101
lines changed

lib/Backend/Func.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//-------------------------------------------------------------------------------------------------------
22
// Copyright (C) Microsoft. All rights reserved.
3+
// Copyright (c) 2021 ChakraCore Project Contributors. All rights reserved.
34
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
45
//-------------------------------------------------------------------------------------------------------
56
#include "Backend.h"
@@ -149,7 +150,7 @@ Func::Func(JitArenaAllocator *alloc, JITTimeWorkItem * workItem,
149150
, m_forInEnumeratorArrayOffset(-1)
150151
, argInsCount(0)
151152
, m_globalObjTypeSpecFldInfoArray(nullptr)
152-
, m_forInEnumeratorForGeneratorSym(nullptr)
153+
, m_generatorFrameSym(nullptr)
153154
#if LOWER_SPLIT_INT64
154155
, m_int64SymPairMap(nullptr)
155156
#endif

lib/Backend/Func.h

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//-------------------------------------------------------------------------------------------------------
22
// Copyright (C) Microsoft. All rights reserved.
3+
// Copyright (c) 2021 ChakraCore Project Contributors. All rights reserved.
34
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
45
//-------------------------------------------------------------------------------------------------------
56
#pragma once
@@ -1063,21 +1064,21 @@ static const unsigned __int64 c_debugFillPattern8 = 0xcececececececece;
10631064
StackSym* m_loopParamSym;
10641065
StackSym* m_bailoutReturnValueSym;
10651066
StackSym* m_hasBailedOutSym;
1066-
StackSym* m_forInEnumeratorForGeneratorSym;
1067+
StackSym* m_generatorFrameSym;
10671068

10681069
public:
10691070
StackSym* tempSymDouble;
10701071
StackSym* tempSymBool;
10711072

1072-
void SetForInEnumeratorSymForGeneratorSym(StackSym* sym)
1073+
void SetGeneratorFrameSym(StackSym* sym)
10731074
{
1074-
Assert(this->m_forInEnumeratorForGeneratorSym == nullptr);
1075-
this->m_forInEnumeratorForGeneratorSym = sym;
1075+
Assert(this->m_generatorFrameSym == nullptr);
1076+
this->m_generatorFrameSym = sym;
10761077
}
10771078

1078-
StackSym* GetForInEnumeratorSymForGeneratorSym() const
1079+
StackSym* GetGeneratorFrameSym() const
10791080
{
1080-
return this->m_forInEnumeratorForGeneratorSym;
1081+
return this->m_generatorFrameSym;
10811082
}
10821083

10831084
// StackSyms' corresponding getters/setters

lib/Backend/IRBuilder.cpp

+30-46
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//-------------------------------------------------------------------------------------------------------
22
// Copyright (C) Microsoft. All rights reserved.
3+
// Copyright (c) 2021 ChakraCore Project Contributors. All rights reserved.
34
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
45
//-------------------------------------------------------------------------------------------------------
56
#include "Backend.h"
@@ -117,6 +118,11 @@ IRBuilder::DoBailOnNoProfile()
117118
return false;
118119
}
119120

121+
if (m_func->GetTopFunc()->GetJITFunctionBody()->IsCoroutine())
122+
{
123+
return false;
124+
}
125+
120126
return true;
121127
}
122128

@@ -1255,7 +1261,7 @@ IRBuilder::EnsureLoopBodyForInEnumeratorArrayOpnd()
12551261
}
12561262

12571263
IR::Opnd *
1258-
IRBuilder::BuildForInEnumeratorOpnd(uint forInLoopLevel)
1264+
IRBuilder::BuildForInEnumeratorOpnd(uint forInLoopLevel, uint32 offset)
12591265
{
12601266
Assert(forInLoopLevel < this->m_func->GetJITFunctionBody()->GetForInLoopDepth());
12611267
if (this->IsLoopBody())
@@ -1270,7 +1276,7 @@ IRBuilder::BuildForInEnumeratorOpnd(uint forInLoopLevel)
12701276
else if (this->m_func->GetJITFunctionBody()->IsCoroutine())
12711277
{
12721278
return IR::IndirOpnd::New(
1273-
this->m_generatorJumpTable.EnsureForInEnumeratorArrayOpnd(),
1279+
this->m_generatorJumpTable.BuildForInEnumeratorArrayOpnd(offset),
12741280
forInLoopLevel * sizeof(Js::ForInObjectEnumerator),
12751281
TyMachPtr,
12761282
this->m_func
@@ -2949,7 +2955,7 @@ IRBuilder::BuildProfiledReg1Unsigned1(Js::OpCode newOpcode, uint32 offset, Js::R
29492955
if (newOpcode == Js::OpCode::InitForInEnumerator)
29502956
{
29512957
IR::RegOpnd * src1Opnd = this->BuildSrcOpnd(R0);
2952-
IR::Opnd * src2Opnd = this->BuildForInEnumeratorOpnd(C1);
2958+
IR::Opnd * src2Opnd = this->BuildForInEnumeratorOpnd(C1, offset);
29532959
IR::Instr *instr = IR::ProfiledInstr::New(Js::OpCode::InitForInEnumerator, nullptr, src1Opnd, src2Opnd, m_func);
29542960
instr->AsProfiledInstr()->u.profileId = profileId;
29552961
this->AddInstr(instr, offset);
@@ -3084,7 +3090,7 @@ IRBuilder::BuildReg1Unsigned1(Js::OpCode newOpcode, uint offset, Js::RegSlot R0,
30843090
{
30853091
IR::Instr *instr = IR::Instr::New(Js::OpCode::InitForInEnumerator, m_func);
30863092
instr->SetSrc1(this->BuildSrcOpnd(R0));
3087-
instr->SetSrc2(this->BuildForInEnumeratorOpnd(C1));
3093+
instr->SetSrc2(this->BuildForInEnumeratorOpnd(C1, offset));
30883094
this->AddInstr(instr, offset);
30893095
return;
30903096
}
@@ -6935,7 +6941,7 @@ IRBuilder::BuildBrReg1Unsigned1(Js::OpCode newOpcode, uint32 offset)
69356941
void
69366942
IRBuilder::BuildBrBReturn(Js::OpCode newOpcode, uint32 offset, Js::RegSlot DestRegSlot, uint32 forInLoopLevel, uint32 targetOffset)
69376943
{
6938-
IR::Opnd *srcOpnd = this->BuildForInEnumeratorOpnd(forInLoopLevel);
6944+
IR::Opnd *srcOpnd = this->BuildForInEnumeratorOpnd(forInLoopLevel, offset);
69396945
IR::RegOpnd * destOpnd = this->BuildDstOpnd(DestRegSlot);
69406946
IR::BranchInstr * branchInstr = IR::BranchInstr::New(newOpcode, destOpnd, nullptr, srcOpnd, m_func);
69416947
this->AddBranchInstr(branchInstr, offset, targetOffset);
@@ -7922,14 +7928,12 @@ IRBuilder::GeneratorJumpTable::BuildJumpTable()
79227928
//
79237929
// s1 = Ld_A prm1
79247930
// s2 = Ld_A s1[offset of JavascriptGenerator::frame]
7925-
// BrNotAddr_A s2 !nullptr $initializationCode
7931+
// BrNotAddr_A s2 !nullptr $jumpTable
79267932
//
79277933
// $createInterpreterStackFrame:
79287934
// call helper
79297935
//
7930-
// $initializationCode:
7931-
// load for-in enumerator address from interpreter stack frame
7932-
//
7936+
// Br $startOfFunc
79337937
//
79347938
// $jumpTable:
79357939
//
@@ -7963,23 +7967,25 @@ IRBuilder::GeneratorJumpTable::BuildJumpTable()
79637967
IR::LabelInstr* functionBegin = IR::LabelInstr::New(Js::OpCode::Label, this->m_func);
79647968
LABELNAMESET(functionBegin, "GeneratorFunctionBegin");
79657969

7966-
IR::LabelInstr* initCode = IR::LabelInstr::New(Js::OpCode::Label, this->m_func);
7967-
LABELNAMESET(initCode, "GeneratorInitializationAndJumpTable");
7970+
IR::LabelInstr* jumpTable = IR::LabelInstr::New(Js::OpCode::Label, this->m_func);
7971+
LABELNAMESET(jumpTable, "GeneratorJumpTable");
79687972

7969-
// BrNotAddr_A s2 nullptr $initializationCode
7970-
IR::BranchInstr* skipCreateInterpreterFrame = IR::BranchInstr::New(Js::OpCode::BrNotAddr_A, initCode, genFrameOpnd, IR::AddrOpnd::NewNull(this->m_func), this->m_func);
7973+
// If there is already a stack frame, generator function has previously begun execution - don't recreate, skip down to jump table
7974+
// BrNotAddr_A s2 nullptr $jumpTable
7975+
IR::BranchInstr* skipCreateInterpreterFrame = IR::BranchInstr::New(Js::OpCode::BrNotAddr_A, jumpTable, genFrameOpnd, IR::AddrOpnd::NewNull(this->m_func), this->m_func);
79717976
this->m_irBuilder->AddInstr(skipCreateInterpreterFrame, this->m_irBuilder->m_functionStartOffset);
79727977

79737978
// Create interpreter stack frame
79747979
IR::Instr* createInterpreterFrame = IR::Instr::New(Js::OpCode::GeneratorCreateInterpreterStackFrame, genFrameOpnd /* dst */, genRegOpnd /* src */, this->m_func);
79757980
this->m_irBuilder->AddInstr(createInterpreterFrame, this->m_irBuilder->m_functionStartOffset);
79767981

7982+
// Having created the frame, skip over the jump table and start executing from the beginning of the function
79777983
IR::BranchInstr* skipJumpTable = IR::BranchInstr::New(Js::OpCode::Br, functionBegin, this->m_func);
79787984
this->m_irBuilder->AddInstr(skipJumpTable, this->m_irBuilder->m_functionStartOffset);
79797985

7980-
// Label to insert any initialization code
7981-
// $initializationCode:
7982-
this->m_irBuilder->AddInstr(initCode, this->m_irBuilder->m_functionStartOffset);
7986+
// Label for start of jumpTable - where we look for the correct Yield resume point
7987+
// $jumpTable:
7988+
this->m_irBuilder->AddInstr(jumpTable, this->m_irBuilder->m_functionStartOffset);
79837989

79847990
// s3 = Ld_A s2[offset of InterpreterStackFrame::m_reader.m_currentLocation]
79857991
IR::RegOpnd* curLocOpnd = IR::RegOpnd::New(TyMachPtr, this->m_func);
@@ -8015,46 +8021,24 @@ IRBuilder::GeneratorJumpTable::BuildJumpTable()
80158021

80168022
this->m_irBuilder->AddInstr(functionBegin, this->m_irBuilder->m_functionStartOffset);
80178023

8018-
// Save these values for later use
8019-
this->m_initLabel = initCode;
8024+
// Save this value for later use
80208025
this->m_generatorFrameOpnd = genFrameOpnd;
8026+
this->m_func->SetGeneratorFrameSym(genFrameOpnd->GetStackSym());
80218027

80228028
return this->m_irBuilder->m_lastInstr;
80238029
}
80248030

8025-
IR::LabelInstr*
8026-
IRBuilder::GeneratorJumpTable::GetInitLabel() const
8027-
{
8028-
Assert(this->m_initLabel != nullptr);
8029-
return this->m_initLabel;
8030-
}
8031-
80328031
IR::RegOpnd*
8033-
IRBuilder::GeneratorJumpTable::CreateForInEnumeratorArrayOpnd()
8034-
{
8035-
Assert(this->m_initLabel != nullptr);
8032+
IRBuilder::GeneratorJumpTable::BuildForInEnumeratorArrayOpnd(uint32 offset)
8033+
{
80368034
Assert(this->m_generatorFrameOpnd != nullptr);
80378035

8038-
IR::RegOpnd* forInEnumeratorOpnd = IR::RegOpnd::New(TyMachPtr, this->m_func);
8039-
IR::Instr* instr = IR::Instr::New(
8040-
Js::OpCode::Ld_A,
8041-
forInEnumeratorOpnd,
8036+
IR::RegOpnd* forInEnumeratorArrayOpnd = IR::RegOpnd::New(TyMachPtr, this->m_func);
8037+
IR::Instr* instr = IR::Instr::New(Js::OpCode::Ld_A, forInEnumeratorArrayOpnd,
80428038
POINTER_OFFSET(this->m_generatorFrameOpnd, Js::InterpreterStackFrame::GetOffsetOfForInEnumerators(), ForInEnumerators),
80438039
this->m_func
80448040
);
8045-
this->m_initLabel->InsertAfter(instr);
8046-
8047-
return forInEnumeratorOpnd;
8048-
}
8049-
8050-
IR::RegOpnd*
8051-
IRBuilder::GeneratorJumpTable::EnsureForInEnumeratorArrayOpnd()
8052-
{
8053-
if (this->m_forInEnumeratorArrayOpnd == nullptr)
8054-
{
8055-
this->m_forInEnumeratorArrayOpnd = this->CreateForInEnumeratorArrayOpnd();
8056-
this->m_func->SetForInEnumeratorSymForGeneratorSym(m_forInEnumeratorArrayOpnd->GetStackSym());
8057-
}
8041+
this->m_irBuilder->AddInstr(instr, offset);
80588042

8059-
return this->m_forInEnumeratorArrayOpnd;
8043+
return forInEnumeratorArrayOpnd;
80608044
}

lib/Backend/IRBuilder.h

+3-19
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//-------------------------------------------------------------------------------------------------------
22
// Copyright (C) Microsoft. All rights reserved.
3+
// Copyright (c) 2021 ChakraCore Project Contributors. All rights reserved.
34
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
45
//-------------------------------------------------------------------------------------------------------
56
#pragma once
@@ -227,7 +228,7 @@ class IRBuilder
227228
IR::RegOpnd * BuildSrcOpnd(Js::RegSlot srcRegSlot, IRType type = TyVar);
228229
IR::AddrOpnd * BuildAuxArrayOpnd(AuxArrayValue auxArrayType, uint32 auxArrayOffset);
229230
IR::Opnd * BuildAuxObjectLiteralTypeRefOpnd(int objectId);
230-
IR::Opnd * BuildForInEnumeratorOpnd(uint forInLoopLevel);
231+
IR::Opnd * BuildForInEnumeratorOpnd(uint forInLoopLevel, uint32 offset);
231232
IR::RegOpnd * EnsureLoopBodyForInEnumeratorArrayOpnd();
232233
private:
233234
uint AddStatementBoundary(uint statementIndex, uint offset);
@@ -366,29 +367,12 @@ class IRBuilder
366367
Func* const m_func;
367368
IRBuilder* const m_irBuilder;
368369

369-
// for-in enumerators are allocated on the heap for jit'd loop body
370-
// and on the stack for all other cases (the interpreter frame will
371-
// reuses them when bailing out). But because we have the concept of
372-
// "bailing in" for generator, reusing enumerators allocated on the stack
373-
// would not work. So we have to allocate them on the generator's interpreter
374-
// frame instead. This operand is loaded as part of the jump table, before we
375-
// jump to any of the resume point.
376-
IR::RegOpnd* m_forInEnumeratorArrayOpnd = nullptr;
377-
378370
IR::RegOpnd* m_generatorFrameOpnd = nullptr;
379371

380-
// This label is used to insert any initialization code that might be needed
381-
// when bailing in and before jumping to any of the resume points.
382-
// As of now, we only need to load the operand for for-in enumerator.
383-
IR::LabelInstr* m_initLabel = nullptr;
384-
385-
IR::RegOpnd* CreateForInEnumeratorArrayOpnd();
386-
387372
public:
388373
GeneratorJumpTable(Func* func, IRBuilder* irBuilder);
389374
IR::Instr* BuildJumpTable();
390-
IR::LabelInstr* GetInitLabel() const;
391-
IR::RegOpnd* EnsureForInEnumeratorArrayOpnd();
375+
IR::RegOpnd* BuildForInEnumeratorArrayOpnd(uint32 offset);
392376
};
393377

394378
GeneratorJumpTable m_generatorJumpTable;

lib/Backend/LinearScan.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//-------------------------------------------------------------------------------------------------------
22
// Copyright (C) Microsoft Corporation and contributors. All rights reserved.
3+
// Copyright (c) 2021 ChakraCore Project Contributors. All rights reserved.
34
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
45
//-------------------------------------------------------------------------------------------------------
56

@@ -5301,7 +5302,7 @@ bool LinearScan::GeneratorBailIn::NeedsReloadingBackendSymWhenBailingIn(StackSym
53015302
{
53025303
// for-in enumerator in generator is loaded as part of the resume jump table.
53035304
// By the same reasoning as `initializedRegs`'s, we don't have to restore this whether or not it's been spilled.
5304-
if (this->func->GetForInEnumeratorSymForGeneratorSym() && this->func->GetForInEnumeratorSymForGeneratorSym()->m_id == sym->m_id)
5305+
if (this->func->GetGeneratorFrameSym() && this->func->GetGeneratorFrameSym()->m_id == sym->m_id)
53055306
{
53065307
return false;
53075308
}

lib/Common/ConfigFlagsList.h

+10-1
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,15 @@ PHASE(All)
673673
#define DEFAULT_CONFIG_ESArrayFindFromLast (false)
674674
#define DEFAULT_CONFIG_ESNullishCoalescingOperator (true)
675675
#define DEFAULT_CONFIG_ESGlobalThis (true)
676+
677+
// Jitting generators has not been tested on ARM
678+
// enabled only for x86 and x64 for now
679+
#ifdef _M_ARM32_OR_ARM64
680+
#define DEFAULT_CONFIG_JitES6Generators (false)
681+
#else
682+
#define DEFAULT_CONFIG_JitES6Generators (true)
683+
#endif
684+
676685
#ifdef COMPILE_DISABLE_ES6RegExPrototypeProperties
677686
// If ES6RegExPrototypeProperties needs to be disabled by compile flag, DEFAULT_CONFIG_ES6RegExPrototypeProperties should be false
678687
#define DEFAULT_CONFIG_ES6RegExPrototypeProperties (false)
@@ -1206,7 +1215,7 @@ FLAGR(Boolean, ESImportMeta, "Enable import.meta keyword", DEFAULT_CONFIG_ESImpo
12061215
FLAGR(Boolean, ESGlobalThis, "Enable globalThis", DEFAULT_CONFIG_ESGlobalThis)
12071216

12081217
// This flag to be removed once JITing generator functions is stable
1209-
FLAGNR(Boolean, JitES6Generators , "Enable JITing of ES6 generators", false)
1218+
FLAGNR(Boolean, JitES6Generators , "Enable JITing of ES6 generators", DEFAULT_CONFIG_JitES6Generators)
12101219

12111220
FLAGNR(Boolean, FastLineColumnCalculation, "Enable fast calculation of line/column numbers from the source.", DEFAULT_CONFIG_FastLineColumnCalculation)
12121221
FLAGR (String, Filename , "Jscript source file", nullptr)

lib/Runtime/Base/FunctionBody.cpp

+5-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//-------------------------------------------------------------------------------------------------------
22
// Copyright (C) Microsoft. All rights reserved.
3+
// Copyright (c) 2021 ChakraCore Project Contributors. All rights reserved.
34
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
45
//-------------------------------------------------------------------------------------------------------
56
#include "RuntimeBasePch.h"
@@ -341,13 +342,13 @@ namespace Js
341342
bool
342343
FunctionBody::SkipAutoProfileForCoroutine() const
343344
{
344-
return this->IsCoroutine() && CONFIG_ISENABLED(Js::JitES6GeneratorsFlag);
345+
return this->IsCoroutine() && CONFIG_FLAG(JitES6Generators);
345346
}
346347

347348
bool
348349
FunctionBody::IsGeneratorAndJitIsDisabled() const
349350
{
350-
return this->IsCoroutine() && !(CONFIG_ISENABLED(Js::JitES6GeneratorsFlag) && !this->GetHasTry() && !this->IsInDebugMode() && !this->IsAsync());
351+
return this->IsCoroutine() && !(CONFIG_FLAG(JitES6Generators) && !this->GetHasTry() && !this->IsInDebugMode() && !this->IsModule());
351352
}
352353

353354
ScriptContext* EntryPointInfo::GetScriptContext()
@@ -7151,8 +7152,7 @@ namespace Js
71517152
!GetScriptContext()->IsScriptContextInDebugMode() &&
71527153
DoInterpreterProfile() &&
71537154
#pragma warning(suppress: 6235) // (<non-zero constant> || <expression>) is always a non-zero constant.
7154-
(!CONFIG_FLAG(NewSimpleJit) || DoInterpreterAutoProfile()) &&
7155-
!IsCoroutine(); // Generator JIT requires bailout which SimpleJit cannot do since it skips GlobOpt
7155+
(!CONFIG_FLAG(NewSimpleJit) || DoInterpreterAutoProfile());
71567156
}
71577157

71587158
bool FunctionBody::DoSimpleJitWithLock() const
@@ -7166,8 +7166,7 @@ namespace Js
71667166
!this->IsInDebugMode() &&
71677167
DoInterpreterProfileWithLock() &&
71687168
#pragma warning(suppress: 6235) // (<non-zero constant> || <expression>) is always a non-zero constant.
7169-
(!CONFIG_FLAG(NewSimpleJit) || DoInterpreterAutoProfile()) &&
7170-
!IsCoroutine(); // Generator JIT requires bailout which SimpleJit cannot do since it skips GlobOpt
7169+
(!CONFIG_FLAG(NewSimpleJit) || DoInterpreterAutoProfile());
71717170
}
71727171

71737172
bool FunctionBody::DoSimpleJitDynamicProfile() const

lib/Runtime/Base/FunctionInfo.h

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//-------------------------------------------------------------------------------------------------------
22
// Copyright (C) Microsoft. All rights reserved.
3+
// Copyright (c) 2021 ChakraCore Project Contributors. All rights reserved.
34
// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
45
//-------------------------------------------------------------------------------------------------------
56
namespace Js
@@ -42,7 +43,8 @@ namespace Js
4243
Method = 0x400000, // The function is a method
4344
ComputedName = 0x800000,
4445
ActiveScript = 0x1000000,
45-
HomeObj = 0x2000000
46+
HomeObj = 0x2000000,
47+
GeneratorWithComplexParams = 0x8000000 // Generator function with non-simple params needs startup yield
4648
};
4749
FunctionInfo(JavascriptMethod entryPoint, Attributes attributes = None, LocalFunctionId functionId = Js::Constants::NoFunctionId, FunctionProxy* functionBodyImpl = nullptr);
4850
FunctionInfo(JavascriptMethod entryPoint, _no_write_barrier_tag, Attributes attributes = None, LocalFunctionId functionId = Js::Constants::NoFunctionId, FunctionProxy* functionBodyImpl = nullptr);
@@ -136,6 +138,7 @@ namespace Js
136138
bool GetBaseConstructorKind() const { return (attributes & Attributes::BaseConstructorKind) != 0; }
137139
bool IsActiveScript() const { return ((this->attributes & Attributes::ActiveScript) != 0); }
138140
void SetIsActiveScript() { attributes = (Attributes)(attributes | Attributes::ActiveScript); }
141+
bool GetGeneratorWithComplexParams() {return (attributes & Attributes::GeneratorWithComplexParams) != 0; }
139142
protected:
140143
FieldNoBarrier(JavascriptMethod) originalEntryPoint;
141144
FieldWithBarrier(FunctionProxy *) functionBodyImpl; // Implementation of the function- null if the function doesn't have a body

0 commit comments

Comments
 (0)